diff --git a/docs/modular-agent-addresses.md b/docs/modular-agent-addresses.md index fde688075..13caba7e7 100644 --- a/docs/modular-agent-addresses.md +++ b/docs/modular-agent-addresses.md @@ -1,12 +1,16 @@ -# Modular Agent Addresses — Liquidity Vault Operator +# Modular Agent Addresses — the worked examples A **Modular Agent Address (MAA)** is a non-custodial agent wallet: a token holder (the *owner*) delegates a limited set of actions to an *agent* key, which can operate the wallet — but provably **cannot move funds out**. The owner keeps full control and withdraws at any time, paying the agent an agreed commission. This is TRUF.NETWORK's analogue of a Safe Module. -This document walks the first end-to-end example: a **Liquidity Vault Operator** — an LP funds -an agent that runs a trading bot on the order books. +This document walks the two end-to-end examples: + +1. a **Liquidity Vault Operator** — an LP funds an agent that runs a trading bot on the order + books; +2. a **Data Provision Agent** — an AI agent creates indexes and provides regular data to them, + paying the network's write fees out of the wallet's escrow. > Roles, in MAA terms: > - **Restricted address (agent):** creates the rule; limited to the rule's allow-list; can @@ -17,7 +21,9 @@ an agent that runs a trading bot on the order books. > The two component keys sign their own transactions; the **MAA** is the wallet they operate. > See the rule store (`048-maa.sql`) and the withdrawal/commission actions (`049-maa-funding.sql`). -## The canonical LP allow-list +## Example 1 — Liquidity Vault Operator + +### The canonical LP allow-list A liquidity-vault agent is allow-listed for exactly the four order-book trading actions, all in the default (`main`) namespace: @@ -31,8 +37,9 @@ the default (`main`) namespace: These are the only powers the bot needs. Deliberately **excluded**: -- **`create_market`** — pays a market-creation fee via `transfer`, which the token boundary - blocks for a restricted agent; market provisioning is not a liquidity-maintenance task. +- **`create_market`** — market provisioning is not a liquidity-maintenance task; the market is + created by an ordinary account. (Its leader-paid creation fee would pass the token boundary's + write-fee carve-out, but the allow-list is the owner's choice of *job*, not of what's possible.) - **`settle_market`** — settlement is permissionless and left to the network. - **Any withdrawal / bridge primitive** — exits are never allow-listed; the owner withdraws through the route-privileged `maa_withdraw` / `maa_bridge_out`, which the agent cannot reach. @@ -40,7 +47,7 @@ These are the only powers the bot needs. Deliberately **excluded**: Body-hash pinning (`body_hash`) is supported by the rule store but left unpinned in this example. -## Why the agent can lock collateral but can't steal +### Why the agent can lock collateral but can't steal Every order-book trade only ever moves the agent wallet's tokens through the erc20 **`lock`** (escrow into the network vault) and **`unlock`** (refunds, fills, settlement) primitives. The @@ -49,7 +56,14 @@ at any call depth, but leaves `lock` / `unlock` open — so allow-listed trading every path that would move funds *out of the network* for the agent is closed. Placing an order escrows collateral; it never sends tokens to an address the agent chooses. -## Lifecycle +There is exactly **one carve-out** in the boundary: a `transfer` whose recipient is the **block +leader's sender address** — the network's write-fee sink (`@leader_sender` in the fee-charging +actions). The recipient is consensus-determined, never a call parameter, so the agent still +cannot steer a single token to an address of its choosing; the worst a malicious agent can do +is spend the wallet's escrow on protocol fees, which is precisely the spending the owner funded +it for. The carve-out is what makes the Data Provision Agent below possible. + +### Lifecycle 1. **Create the rule** — the agent signs `maa_create_rule(salt, 'bps', fee_bps, fee_flat, namespaces, actions, body_hashes)` with the allow-list above, and gets a `rule_id`. @@ -69,7 +83,7 @@ escrows collateral; it never sends tokens to an address the agent chooses. **free** balance, so collateral in open orders must be unwound (`cancel_order`) first — after which the LP can recover everything. -## Monitoring getter +### Monitoring getter `get_order_events_by_wallet` (migration `050-order-book-event-queries.sql`) exposes the order book's per-event history for any wallet address — the surface an owner uses to watch an agent @@ -90,7 +104,7 @@ get_order_events_by_wallet( `ask_changed`, `cancelled`, `direct_buy_fill`, `direct_sell_fill`, `mint_fill`, `burn_fill`, `settled`. -## End-to-end test +### End-to-end test `tests/streams/maa/lp_vault_test.go` drives the whole lifecycle against the real order book: @@ -103,3 +117,53 @@ get_order_events_by_wallet( - **`testLPVaultAgentCannotDrain`** — the agent can place allow-listed orders, but every attempt to withdraw or bridge funds out is blocked at the token boundary; nothing moves and no commission is skimmed. + +## Example 2 — Data Provision Agent + +An AI agent creates new indexes (streams) and provides regular data to them, running entirely +as its agent wallet. The owner fills the wallet with **TRUF** — the agent's working budget — +secure that the agent can spend it only on the network's write fees, never take it. + +### The data-provision allow-list + +| Namespace | Action | Purpose | +|-----------|------------------|---------------------------------------------------| +| `main` | `create_streams` | Create new indexes (owned by the agent wallet) | +| `main` | `insert_records` | Provide data to those indexes | + +Both actions charge **caller-keyed** bridge fees — `create_streams` 100 TRUF per stream, +`insert_records` a flat 1 TRUF per transaction once the wallet is enrolled in +`system:fee_required` — so with `@caller` rewritten to the MAA they debit the **agent +wallet's own escrow**, with zero fee code specific to MAA. The fee `transfer` targets +`@leader_sender`, which is exactly the token boundary's write-fee carve-out; any other +`transfer` recipient remains blocked for the restricted agent. + +The streams the agent creates belong to the **MAA**, not to the agent's own key: the agent +wallet is the data provider of record, so replacing the agent (a new rule + wallet) never +strands the data identity with a key the owner doesn't control. + +### Lifecycle + +1. **Create the rule** — the agent signs `maa_create_rule` with the two-action allow-list. +2. **Join** — the owner signs `maa_join(rule_id)` and gets the wallet address. +3. **Fill up** — the owner sends TRUF to the MAA address (a normal bridged-token transfer). +4. **Provide** — the agent submits `create_streams` / `insert_records` *as the MAA*; every fee + comes out of the wallet's TRUF escrow and lands with the block leader. An underfunded wallet + fails closed: the action reverts before any state is written. +5. **Monitor** — anyone can read the provided data back (`get_record`); the rule/audit getters + (`maa_get_instance`, `maa_get_events`, …) cover the wallet itself. +6. **Withdraw any time** — the owner signs `maa_withdraw($bridge, $amount)` against the TRUF + bridge and recovers the un-spent escrow, net of the agent's commission. + +### End-to-end test + +`tests/streams/maa/data_agent_test.go`: + +- **`testDataAgentLifecycle`** — the agent creates an index and provides data as the MAA, the + 100-TRUF and 1-TRUF fees coming out of the wallet's escrow and landing with the block leader; + the owner reads the data back and withdraws the rest, paying the commission. +- **`testDataAgentInsufficientEscrow`** — with less than the per-stream fee on the wallet, the + agent's `create_streams` reverts; no stream, no partial fee. +- **`testDataAgentCannotExfiltrate`** — the agent can do its fee-paying job, but withdrawing, + bridging out, and raw transfers to any non-leader recipient are all blocked at the token + boundary; nothing moves and no commission is skimmed. diff --git a/go.mod b/go.mod index b7bcf62c5..3df8c8a15 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,8 @@ require ( github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.37.0 - github.com/trufnetwork/kwil-db v0.10.3-0.20260605080707-350a1bb51469 - github.com/trufnetwork/kwil-db/core v0.4.3-0.20260605080707-350a1bb51469 + github.com/trufnetwork/kwil-db v0.10.3-0.20260610105042-7d3dce4d34b4 + github.com/trufnetwork/kwil-db/core v0.4.3-0.20260610105042-7d3dce4d34b4 github.com/trufnetwork/sdk-go v0.6.4-0.20260224122406-a741343e2f37 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa diff --git a/go.sum b/go.sum index 24ca89bed..5eafcc402 100644 --- a/go.sum +++ b/go.sum @@ -1240,80 +1240,10 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= -github.com/trufnetwork/kwil-db v0.10.3-0.20260406143732-e25a390e62bb h1:VbAdBjAfxxj/yrxRaHNgzDylacoIPA7QQb/c8hHLldM= -github.com/trufnetwork/kwil-db v0.10.3-0.20260406143732-e25a390e62bb/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260413125950-e0bc3b09a211 h1:safm8TC6MOf2k6oXYboHt4axDXHStp6vs+C+eKOvnqo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260413125950-e0bc3b09a211/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260413192528-5fa840da03a8 h1:JF+y8fZkrtkGdT541pcIyvn8yYwD5tanPXEM3LpkAdM= -github.com/trufnetwork/kwil-db v0.10.3-0.20260413192528-5fa840da03a8/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414063848-ca8fcd878e35 h1:BhcLm/iIgSyjvGGhnsccTjDqYpIV2j8QJRLRQPtM/Lk= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414063848-ca8fcd878e35/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414064815-f74d2ccc30ae h1:Vf3UnYJDgCptazYdkwbTVGZ8PM9Xtd8kXQKN8PlPREs= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414064815-f74d2ccc30ae/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414094734-bb696e25c46d h1:m5olARBYODr9yldv1wLZcKIaPcshTlKXC1td0vsz4Q0= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414094734-bb696e25c46d/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414100322-facad37b3eb2 h1:mC687oWOo0YLjYkVQsRr5W83xne2RojzaulsPjQB7uw= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414100322-facad37b3eb2/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414105203-b684b866ef30 h1:L2E1whVeekeZw92CBKlKOeqKZASUWg79XtrVtjQ/BYk= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414105203-b684b866ef30/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414113323-696cc38f53b8 h1:HuR9awZ/6r8YepSb8C5XRoSa7T++c5nr2jb9irxMTGU= -github.com/trufnetwork/kwil-db v0.10.3-0.20260414113323-696cc38f53b8/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424141846-c520d3ebfffd h1:kkBMIocp4Ie7kHcdMArnwHRKUSIjdacRq+sV3jg6klY= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424141846-c520d3ebfffd/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424162927-ac1b5755e3b7 h1:odTjhGG7GlhyKqsSQtMdPsGqgY6YJTIwIyTu5IIxArg= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424162927-ac1b5755e3b7/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424174214-23b4652cc326 h1:B6l9wp6RDT6QQoejLN8fVXNW/lt0h0Qdbqc0KJsjOrI= -github.com/trufnetwork/kwil-db v0.10.3-0.20260424174214-23b4652cc326/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425015148-65dd9427650c h1:m6xLA3GV7BbNRxMTVlcdSIdFnIG6Z/6pdv3VeXoGhwU= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425015148-65dd9427650c/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425032526-01958405eb8f h1:ebwnykLqpEYG6CQlnrNyMCCbYcrsV8RxW1y6nGTjWAE= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425032526-01958405eb8f/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425051255-2adc7ffbb5c8 h1:QU7TBID+UlwjOjIC0AsQmUYOE/68B83nHsW68Prz46Y= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425051255-2adc7ffbb5c8/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425175323-4e873ca0a470 h1:zk1TItTGbyC2AOE4zBn+WzHs6hPo6ORokM7dnZaxfgk= -github.com/trufnetwork/kwil-db v0.10.3-0.20260425175323-4e873ca0a470/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260427035100-12ed615d43cb h1:i+iaJUY8J+mLDr5+xkH7Ju+3d7ZADgwiK7+z7LsIC4o= -github.com/trufnetwork/kwil-db v0.10.3-0.20260427035100-12ed615d43cb/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260502065007-81722689e25b h1:fkoEUDIFm3Amq58r5rAzT2g8E5tM+qw0lC41ZGSFpoc= -github.com/trufnetwork/kwil-db v0.10.3-0.20260502065007-81722689e25b/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260504105445-236471e6a1fe h1:hEW7eFTX3gS+WzhdQrh6xR/Zzzub25Zy/kdkosAljFQ= -github.com/trufnetwork/kwil-db v0.10.3-0.20260504105445-236471e6a1fe/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db v0.10.3-0.20260605080707-350a1bb51469 h1:1yHa+gMACjnKSk1GO2Szy8kCgPkqGu4zF9ZdOK3Dbl8= -github.com/trufnetwork/kwil-db v0.10.3-0.20260605080707-350a1bb51469/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260406143732-e25a390e62bb h1:tghQHOXYDHwjBmhEVAQESPTvf3O9Oq/1qzow5aKE81c= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260406143732-e25a390e62bb/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260413125950-e0bc3b09a211 h1:h5HpwEqbPDEo4uGYz7ZUTfQ9tJBKyHaCmtwk2boB7Xs= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260413125950-e0bc3b09a211/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260413192528-5fa840da03a8 h1:InMod3EA4MBbFmgvyduItKXktoRmuSpHuHg973KnCJg= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260413192528-5fa840da03a8/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414064815-f74d2ccc30ae h1:MQfmv8ApXqhiqwMyki+GAzraMiWqigH4nJVgoECJTeg= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414064815-f74d2ccc30ae/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414094734-bb696e25c46d h1:ACToMi9+ksuGBSDe2/GTT7iDhh0Cd7AANfn/lQBFJgI= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414094734-bb696e25c46d/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414100322-facad37b3eb2 h1:ADKw8OTz6+/1vZMujY8g8bkZ57ARkgn8wMp9mAXAFhA= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414100322-facad37b3eb2/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414105203-b684b866ef30 h1:9vaEXG8LRi0WVij3WvULGEsKquPZnV+XKOH3ASnjFnA= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414105203-b684b866ef30/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414113323-696cc38f53b8 h1:fzkcURTH5LlSQpKbm5CezXNmJg8SMltcfYhblHY+HZA= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260414113323-696cc38f53b8/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260424162927-ac1b5755e3b7 h1:uxyGGG2nkL6J0aSvY+rEEvijVQjzCU5US68h72OgTn0= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260424162927-ac1b5755e3b7/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260424174214-23b4652cc326 h1:85U4+Hen5S8jg/1n9WmPtqb5F+HVsNia3jev8TWePLE= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260424174214-23b4652cc326/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425015148-65dd9427650c h1:JbIhZag6hhctTvcgyedf8gHXk56d6z2LSYh1/uNa5WU= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425015148-65dd9427650c/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425032526-01958405eb8f h1:sUXarAL3agU5APqin8hrA4uwgQxfjKP3v5pvrzBcOaU= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425032526-01958405eb8f/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425051255-2adc7ffbb5c8 h1:7m3S++3CqQlNBvUiRr9rHuTtUSRyjcyTFrhOhu56d/0= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260425051255-2adc7ffbb5c8/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260427035100-12ed615d43cb h1:aOXkiXQEjEFvIcYp175aOO7OFDHZBYuFtX6K/xEbeqg= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260427035100-12ed615d43cb/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260502065007-81722689e25b h1:bpX85oQ8F/chXJeXu2hCiXrO22gAN7pHjWaO+l2r9+c= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260502065007-81722689e25b/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260504105445-236471e6a1fe h1:0wvs4osJSaf+gm7yzSAzMHo+VL7KPRkyvHec8hglzkg= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260504105445-236471e6a1fe/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260605080707-350a1bb51469 h1:FXAeaGYdVrW/K6qpRDLtQaIlMkeH0ADUsMVKg5GrJ1o= -github.com/trufnetwork/kwil-db/core v0.4.3-0.20260605080707-350a1bb51469/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= +github.com/trufnetwork/kwil-db v0.10.3-0.20260610105042-7d3dce4d34b4 h1:paJ0TeAcnyaw6C2vhmNi+w/Bacx4G4j5IR8i7VbImFQ= +github.com/trufnetwork/kwil-db v0.10.3-0.20260610105042-7d3dce4d34b4/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= +github.com/trufnetwork/kwil-db/core v0.4.3-0.20260610105042-7d3dce4d34b4 h1:TIQQDEBSH6g2yXRGnKTNEbmKIxfkLd7dSjNPwHQfHmU= +github.com/trufnetwork/kwil-db/core v0.4.3-0.20260610105042-7d3dce4d34b4/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2 h1:DCq8MzbWH0wZmICNmMVsSzUHUPl+2vqRhluEABjxl88= github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2/go.mod h1:Y0MJpPp9QXU5vC6Gpoilql2NkgmGNcbHm9HYC2v2N8s= github.com/trufnetwork/sdk-go v0.6.4-0.20260224122406-a741343e2f37 h1:VD/GWxLTshaXpLukEc1SXbG7QA9HrFzF8JvxJAJ/x7Q= diff --git a/tests/streams/maa/data_agent_test.go b/tests/streams/maa/data_agent_test.go new file mode 100644 index 000000000..de9257462 --- /dev/null +++ b/tests/streams/maa/data_agent_test.go @@ -0,0 +1,358 @@ +//go:build kwiltest + +package maa + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + "github.com/trufnetwork/kwil-db/common" + "github.com/trufnetwork/kwil-db/core/crypto" + coreauth "github.com/trufnetwork/kwil-db/core/crypto/auth" + kwilTypes "github.com/trufnetwork/kwil-db/core/types" + erc20bridge "github.com/trufnetwork/kwil-db/node/exts/erc20-bridge/erc20" + orderedsync "github.com/trufnetwork/kwil-db/node/exts/ordered-sync" + kwilTesting "github.com/trufnetwork/kwil-db/testing" + "github.com/trufnetwork/node/internal/migrations" + testutils "github.com/trufnetwork/node/tests/streams/utils" + testerc20 "github.com/trufnetwork/node/tests/streams/utils/erc20" + "github.com/trufnetwork/node/tests/streams/utils/feefund" + "github.com/trufnetwork/node/tests/streams/utils/setup" + "github.com/trufnetwork/sdk-go/core/util" +) + +// DATA PROVISION AGENT — the second end-to-end MAA example (0MainGoal.md Example 2). +// +// An AI agent (the restricted key) creates new indexes and provides regular data to them, +// running entirely AS its agent wallet (the MAA). The owner (the unrestricted key) "fills up" +// the MAA with TRUF; the agent's job costs TRUF — create_streams charges 100 TRUF per stream +// and insert_records a flat 1 TRUF once the wallet is enrolled in system:fee_required — and +// both fees are caller-keyed bridge transfers, so with @caller rewritten to the MAA they +// debit the MAA's OWN escrow with zero new fee code. The owner stays secure that the agent +// can never exfiltrate the funds: the erc20 MAARestricted boundary blocks every fund exit +// for the restricted role, carving out ONLY the protocol write-fee — a transfer whose +// recipient is the block leader's sender address (@leader_sender), consensus-determined and +// never agent-chosen. +// +// The agent's allow-list is exactly the two data-provision actions in the default ("main") +// namespace. Unlike the LP-vault example (whose fee-paying create_market had to stay OFF the +// allow-list before the leader-fee carve-out existed), fee-paying actions are the POINT here. + +var ( + daNamespaces = []string{"main", "main"} + daActions = []string{"create_streams", "insert_records"} + daBodyHashes = [][]byte{nil, nil} +) + +// Independent ordered-sync points for the data-agent funding (the lp_vault tests use +// 7000/8000; withdraw tests low single digits). +var ( + daTRUFPoint int64 = 9000 + daTRUFPrev *int64 +) + +const ( + daInsertFee = "1000000000000000000" // flat 1 TRUF per insert_records tx (003, fee_required-gated) +) + +func TestMAADataAgent(t *testing.T) { + testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ + Name: "MAA_DataAgent", + SeedStatements: migrations.GetSeedScriptStatements(), + FunctionTests: []kwilTesting.TestFunc{ + testDataAgentLifecycle(t), + testDataAgentInsufficientEscrow(t), + testDataAgentCannotExfiltrate(t), + }, + }, testutils.GetTestOptionsWithCache()) +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// daResetFunding clears the ordered-sync cursor and the TRUF deposit chain; call once at the +// start of each function test (each gets a fresh DB). +func daResetFunding() { + orderedsync.ForTestingReset() + daTRUFPrev = nil +} + +// daFundTRUF credits `to` on the hoodi_tt (TRUF) bridge, chaining to the previous deposit so +// several wallets can be funded in one test. +func daFundTRUF(t *testing.T, ctx context.Context, platform *kwilTesting.Platform, to, amount string) { + t.Helper() + daTRUFPoint++ + p := daTRUFPoint + require.NoError(t, testerc20.InjectERC20Transfer(ctx, platform, wdChain, wdTRUFEscrow, wdTRUFERC20, to, to, amount, p, daTRUFPrev)) + daTRUFPrev = &p +} + +// daLeader generates the block proposer for a function test and returns its key with the +// eth address @leader_sender resolves to — the write-fee recipient. +func daLeader(t *testing.T) (*crypto.Secp256k1PublicKey, string) { + t.Helper() + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + require.NoError(t, err) + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + addr := util.Unsafe_NewEthereumAddressFromString("0x" + hex.EncodeToString(crypto.EthereumAddressFromPubKey(pub))) + return pub, addr.Address() +} + +// daCallAsMAAIn is callAsMAA with a block proposer in context: the fee-charging actions +// resolve @leader_sender from it (NULL proposer makes them ERROR before the fee transfer). +// Everything else reproduces the maa_exec route's inner call — @caller rewritten to the MAA, +// the signer's role carried as TxContext.MAARestricted, and the signer's authenticator. +func daCallAsMAAIn(ctx context.Context, platform *kwilTesting.Platform, maa util.EthereumAddress, restricted bool, leader *crypto.Secp256k1PublicKey, namespace, action string, args []any) error { + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{Height: 1, Proposer: leader}, + Signer: maa.Bytes(), + Caller: maa.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + MAARestricted: restricted, + } + res, err := platform.Engine.Call(&common.EngineContext{TxContext: tx}, platform.DB, namespace, action, args, func(*common.Row) error { return nil }) + if err != nil { + return err + } + return res.Error +} + +// daCallAsMAA is daCallAsMAAIn against the default ("main") namespace. +func daCallAsMAA(ctx context.Context, platform *kwilTesting.Platform, maa util.EthereumAddress, restricted bool, leader *crypto.Secp256k1PublicKey, action string, args []any) error { + return daCallAsMAAIn(ctx, platform, maa, restricted, leader, "", action, args) +} + +// createDARule registers the data-provision allow-list rule, signed by the restricted agent. +func createDARule(t *testing.T, ctx context.Context, platform *kwilTesting.Platform, restricted util.EthereumAddress, feeBps int64, salt []byte) []byte { + t.Helper() + var ruleID []byte + err := callAs(ctx, platform, restricted, "maa_create_rule", []any{ + salt, "bps", feeBps, dec(t, "0"), + daNamespaces, daActions, daBodyHashes, + }, func(row *common.Row) error { + ruleID = append([]byte(nil), row.Values[0].([]byte)...) + return nil + }) + require.NoError(t, err, "maa_create_rule (data-provision allow-list) should succeed") + require.Len(t, ruleID, 32) + return ruleID +} + +// dec36 parses a NUMERIC(36,18) literal — the primitive-record value type. +func dec36(t *testing.T, s string) *kwilTypes.Decimal { + t.Helper() + d, err := kwilTypes.ParseDecimalExplicit(s, 36, 18) + require.NoError(t, err) + return d +} + +// daRecord is one (event_time, value) row read back from a stream. +type daRecord struct { + eventTime int64 + value string +} + +// readRecords reads a stream's records via the public get_record query — the owner's (or +// anyone's) view of the data the agent provided. The explicit [0, 10^9] range matters: NULL +// from/to means "latest record only". +func readRecords(t *testing.T, ctx context.Context, platform *kwilTesting.Platform, reader util.EthereumAddress, dataProvider, streamID string) []daRecord { + t.Helper() + var out []daRecord + require.NoError(t, callAs(ctx, platform, reader, "get_record", + []any{dataProvider, streamID, int64(0), int64(1000000000), nil, false}, + func(row *common.Row) error { + out = append(out, daRecord{ + eventTime: row.Values[0].(int64), + value: row.Values[1].(*kwilTypes.Decimal).String(), + }) + return nil + })) + return out +} + +// streamOwner returns the data_provider recorded for a stream ("" when the stream is absent). +func streamOwner(t *testing.T, ctx context.Context, platform *kwilTesting.Platform, streamID string) string { + t.Helper() + tx := &common.TxContext{Ctx: ctx, BlockContext: &common.BlockContext{Height: 1}, TxID: platform.Txid()} + owner := "" + require.NoError(t, platform.Engine.Execute(&common.EngineContext{TxContext: tx}, platform.DB, + `SELECT data_provider FROM streams WHERE stream_id = $sid`, + map[string]any{"$sid": streamID}, + func(row *common.Row) error { + owner = row.Values[0].(string) + return nil + })) + return owner +} + +// --------------------------------------------------------------------------- +// tests +// --------------------------------------------------------------------------- + +// testDataAgentLifecycle: the full happy path. The agent creates an index and provides data +// to it AS the MAA, every fee coming out of the MAA's TRUF escrow (100 TRUF per stream; +// 1 TRUF per insert once the wallet is enrolled in system:fee_required) and landing with the +// block leader; the owner reads the provided data back and withdraws the remaining escrow at +// any time, paying the agent its commission. +func testDataAgentLifecycle(t *testing.T) func(context.Context, *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + restricted := util.Unsafe_NewEthereumAddressFromString(restrictedHex) + unrestricted := util.Unsafe_NewEthereumAddressFromString(unrestrictedHex) + platform.Deployer = restricted.Bytes() + + require.NoError(t, erc20bridge.ForTestingInitializeExtension(ctx, platform)) + daResetFunding() + leader, leaderAddr := daLeader(t) + + // Agent registers the data-provision rule; the owner joins and fills the MAA with + // 250 TRUF — the agent's working budget for fees, never the agent's to take. + ruleID := createDARule(t, ctx, platform, restricted, 250, repeat(0xab, 32)) + maa := joinRule(t, ctx, platform, unrestricted, ruleID) + maaAddr := addrFromBytes(maa) + daFundTRUF(t, ctx, platform, maaAddr.Address(), "250000000000000000000") // 250 TRUF + require.Equal(t, "250000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address())) + + // 1) THE AGENT CREATES AN INDEX. create_streams runs AS the MAA: the 100-TRUF + // per-stream fee debits the MAA's escrow and lands with the block leader — the + // one fund movement the restricted boundary permits. The MAA is auto-onboarded + // as a data provider and owns the stream. + streamID := "stdataagent000000000000000000001" + require.NoError(t, daCallAsMAA(ctx, platform, maaAddr, true /* restricted agent */, leader, + "create_streams", []any{[]string{streamID}, []string{"primitive"}}), + "the restricted agent must be able to create a stream as the MAA") + require.Equal(t, "150000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address()), + "the 100-TRUF stream fee must come out of the MAA escrow") + require.Equal(t, feefund.StreamCreationFeeWei, balanceTRUF(t, ctx, platform, leaderAddr), + "the stream fee must land with the block leader") + require.Equal(t, maaAddr.Address(), streamOwner(t, ctx, platform, streamID), + "the stream belongs to the agent wallet, not the agent's own key") + + // 2) THE AGENT PROVIDES DATA. Before fee_required enrollment the insert is free — + // today's phased-rollout default. + require.NoError(t, daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, + "insert_records", []any{ + []string{maaAddr.Address()}, []string{streamID}, []int64{100}, []*kwilTypes.Decimal{dec36(t, "42.5")}, + }), + "the agent must be able to insert records into its wallet's stream") + require.Equal(t, "150000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address()), + "un-enrolled wallets pay no insert fee (phased rollout)") + + // 3) Once the wallet is enrolled in system:fee_required, the flat 1-TRUF write fee + // applies — and it too comes out of the MAA escrow. + require.NoError(t, setup.AddMemberToRoleBypass(ctx, platform, "system", "fee_required", maaAddr.Address())) + require.NoError(t, daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, + "insert_records", []any{ + []string{maaAddr.Address()}, []string{streamID}, []int64{200}, []*kwilTypes.Decimal{dec36(t, "43.75")}, + }), + "the enrolled agent must still be able to insert, paying the write fee from escrow") + require.Equal(t, "149000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address()), + "the 1-TRUF write fee must come out of the MAA escrow") + require.Equal(t, "101000000000000000000", balanceTRUF(t, ctx, platform, leaderAddr), + "the write fee must land with the block leader: 100 + 1") + + // 4) THE DATA IS PROVIDED: anyone (here the owner) reads the records back. + records := readRecords(t, ctx, platform, unrestricted, maaAddr.Address(), streamID) + require.Equal(t, []daRecord{ + {eventTime: 100, value: "42.500000000000000000"}, + {eventTime: 200, value: "43.750000000000000000"}, + }, records, "the owner sees the data the agent provided") + + // 5) THE OWNER WITHDRAWS the un-spent escrow at any time, paying the 2.5% + // commission: 149 × 2.5% = 3.725 TRUF to the agent, the rest to the owner. + require.NoError(t, callAsMAA(ctx, platform, maaAddr, false /* unrestricted owner */, "maa_withdraw", + []any{wdTRUFBridge, dec(t, "149000000000000000000")}), + "the owner must be able to withdraw the remaining escrow any time") + require.Equal(t, "0", balanceTRUF(t, ctx, platform, maaAddr.Address()), "the agent wallet is drained") + require.Equal(t, "3725000000000000000", balanceTRUF(t, ctx, platform, restrictedHex), "agent earns 2.5% of 149 = 3.725 TRUF") + require.Equal(t, "145275000000000000000", balanceTRUF(t, ctx, platform, unrestrictedHex), "owner recovers the 145.275 TRUF remainder") + require.Equal(t, 1, countWithdrawEvents(t, ctx, platform, unrestricted, ruleID), "the exit is audited") + return nil + } +} + +// testDataAgentInsufficientEscrow: the escrow-paid-fee path fails closed. With less than the +// per-stream fee on the wallet, the agent's create_streams reverts before any state is +// written — no stream, no partial fee, nothing for the leader. +func testDataAgentInsufficientEscrow(t *testing.T) func(context.Context, *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + restricted := util.Unsafe_NewEthereumAddressFromString(restrictedHex) + unrestricted := util.Unsafe_NewEthereumAddressFromString(unrestrictedHex) + platform.Deployer = restricted.Bytes() + + require.NoError(t, erc20bridge.ForTestingInitializeExtension(ctx, platform)) + daResetFunding() + leader, leaderAddr := daLeader(t) + + ruleID := createDARule(t, ctx, platform, restricted, 250, repeat(0xab, 32)) + maa := joinRule(t, ctx, platform, unrestricted, ruleID) + maaAddr := addrFromBytes(maa) + daFundTRUF(t, ctx, platform, maaAddr.Address(), "50000000000000000000") // 50 TRUF < the 100-TRUF fee + + streamID := "stdataagent000000000000000000002" + err := daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, + "create_streams", []any{[]string{streamID}, []string{"primitive"}}) + require.Error(t, err, "create_streams must revert when the escrow cannot cover the fee") + require.ErrorContains(t, err, "Insufficient balance for stream creation") + + require.Equal(t, "50000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address()), "the failed create must charge nothing") + require.Equal(t, "0", balanceTRUF(t, ctx, platform, leaderAddr), "the leader must receive nothing") + require.Equal(t, "", streamOwner(t, ctx, platform, streamID), "no stream row may exist") + return nil + } +} + +// testDataAgentCannotExfiltrate: the safety promise the owner relies on when filling up the +// wallet. The agent can do its (fee-paying) job, but every path that would move escrow to an +// agent-chosen destination is blocked at the erc20 token boundary — the leader-fee carve-out +// does not open a single exfiltration route. +func testDataAgentCannotExfiltrate(t *testing.T) func(context.Context, *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + restricted := util.Unsafe_NewEthereumAddressFromString(restrictedHex) + unrestricted := util.Unsafe_NewEthereumAddressFromString(unrestrictedHex) + platform.Deployer = restricted.Bytes() + + require.NoError(t, erc20bridge.ForTestingInitializeExtension(ctx, platform)) + daResetFunding() + leader, _ := daLeader(t) + + ruleID := createDARule(t, ctx, platform, restricted, 250, repeat(0xab, 32)) + maa := joinRule(t, ctx, platform, unrestricted, ruleID) + maaAddr := addrFromBytes(maa) + daFundTRUF(t, ctx, platform, maaAddr.Address(), "250000000000000000000") + _ = ruleID // rule audit is covered by testDataAgentLifecycle + + // Sanity: the agent CAN do its job — the fee-paying create succeeds. + require.NoError(t, daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, + "create_streams", []any{[]string{"stdataagent000000000000000000003"}, []string{"primitive"}})) + require.Equal(t, "150000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address())) + + // But the agent CANNOT withdraw — the commission transfer leg targets the agent, + // not the leader, so the boundary rejects it. + err := daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, "maa_withdraw", + []any{wdTRUFBridge, dec(t, "10000000000000000000")}) + require.Error(t, err, "a restricted agent must not be able to withdraw") + require.ErrorContains(t, err, "restricted agent (MAA) execution") + + // Nor bridge escrow off to L1 — the carve-out is transfer-to-leader only. + require.Error(t, daCallAsMAA(ctx, platform, maaAddr, true /* agent */, leader, "maa_bridge_out", + []any{wdTRUFBridge, dec(t, "10000000000000000000"), nil}), + "a restricted agent must not be able to bridge funds out") + + // Nor move tokens to an address of its choosing (its own key here) via the bridge's + // raw transfer — the exact primitive the fee uses, with a non-leader recipient. + err = daCallAsMAAIn(ctx, platform, maaAddr, true /* agent */, leader, "hoodi_tt", "transfer", + []any{restrictedHex, dec(t, "10000000000000000000")}) + require.Error(t, err, "a restricted agent must not be able to transfer to itself") + require.ErrorContains(t, err, "restricted agent (MAA) execution") + + // Nothing moved, no commission skimmed. + require.Equal(t, "150000000000000000000", balanceTRUF(t, ctx, platform, maaAddr.Address()), "blocked exits move nothing") + require.Equal(t, "0", balanceTRUF(t, ctx, platform, restrictedHex), "no commission was paid on the blocked attempts") + return nil + } +} diff --git a/tests/streams/maa/lp_vault_test.go b/tests/streams/maa/lp_vault_test.go index 3baf2c35b..150d879ea 100644 --- a/tests/streams/maa/lp_vault_test.go +++ b/tests/streams/maa/lp_vault_test.go @@ -38,9 +38,10 @@ import ( // leaving collateral lock/unlock open so allow-listed trading survives. // // The bot's allow-list is the four real order-book trading actions in the default ("main") -// namespace; create_market is deliberately NOT allow-listed (it pays a fee via transfer, which -// the boundary blocks for the restricted role) and settle_market is left to the network. The -// market here is created by an ordinary market-maker account, not the agent. +// namespace; create_market is deliberately NOT allow-listed (market provisioning is not a +// liquidity-maintenance task — though its leader-paid fee would pass the boundary's write-fee +// carve-out, see data_agent_test.go) and settle_market is left to the network. The market here +// is created by an ordinary market-maker account, not the agent. // lpActions is the canonical liquidity-vault-operator allow-list: place + amend + cancel on the // order book, nothing that can move funds out. diff --git a/tests/streams/other/other_test.go b/tests/streams/other/other_test.go index bd34634a5..80faa73b8 100644 --- a/tests/streams/other/other_test.go +++ b/tests/streams/other/other_test.go @@ -7,13 +7,16 @@ import ( "github.com/trufnetwork/node/internal/migrations" testutils "github.com/trufnetwork/node/tests/streams/utils" + "github.com/trufnetwork/node/tests/streams/utils/feefund" "github.com/trufnetwork/node/tests/streams/utils/procedure" "github.com/trufnetwork/node/tests/streams/utils/setup" + "github.com/trufnetwork/node/tests/streams/utils/testctx" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/trufnetwork/kwil-db/common" + coreauth "github.com/trufnetwork/kwil-db/core/crypto/auth" kwilTesting "github.com/trufnetwork/kwil-db/testing" "github.com/trufnetwork/sdk-go/core/types" "github.com/trufnetwork/sdk-go/core/util" @@ -29,7 +32,7 @@ var defaultStreamLocator = types.StreamLocator{ // TestAddressValidation tests that all referenced addresses must be lowercased and valid EVM addresses starting with `0x`. func TestAddressValidation(t *testing.T) { testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ - Name: "address_validation_test", + Name: "address_validation_test", SeedStatements: migrations.GetSeedScriptStatements(), FunctionTests: []kwilTesting.TestFunc{ func(ctx context.Context, platform *kwilTesting.Platform) error { @@ -53,10 +56,28 @@ func TestAddressValidation(t *testing.T) { t.Run("MissingPrefix", testutils.WithTx(platform, func(t *testing.T, txPlatform *kwilTesting.Platform) { invalidAddress1 := "0000000000000000000000000000000000000001" - // Test stream creation with invalid address - err := setup.UntypedCreateStream(ctx, txPlatform, defaultStreamLocator.StreamId.String(), invalidAddress1, string(setup.ContractTypePrimitive)) - assert.Error(t, err, "address without 0x prefix should be rejected") - // The system should reject this invalid address (either during role check or address validation) + // The Go-side helpers parse-and-normalize their address argument, so a + // prefix-less form never reaches the network through them — and since + // create_streams became permissionless (#1384) there is no role check + // left to trip on it either. Drive create_stream with the raw string as + // @caller so the system's own check_ethereum_address is what rejects it. + // The fee gate runs before address validation and resolves hex with or + // without the prefix, so fund the canonical form to prove the rejection + // is the address check, not an empty balance. + err := feefund.EnsureWalletFunded(ctx, txPlatform, "0x"+invalidAddress1, feefund.StreamCreationFeeWei) + require.NoError(t, err, "failed to fund the caller's stream fee") + + engineCtx := &common.EngineContext{TxContext: testctx.NewTxContextWithAuth( + ctx, txPlatform, []byte(invalidAddress1), invalidAddress1, coreauth.EthPersonalSignAuth, 1)} + res, callErr := txPlatform.Engine.Call(engineCtx, txPlatform.DB, "", "create_stream", + []any{defaultStreamLocator.StreamId.String(), string(setup.ContractTypePrimitive)}, + func(*common.Row) error { return nil }) + if callErr == nil && res != nil { + callErr = res.Error + } + assert.Error(t, callErr, "address without 0x prefix should be rejected") + assert.ErrorContains(t, callErr, "Invalid data provider address", + "the rejection must come from the system's address validation") })) // Test invalid address - wrong length @@ -89,7 +110,7 @@ func TestAddressValidation(t *testing.T) { // TestStreamIDValidation tests that stream ids must respect the following regex: `^st[a-z0-9]{30}$` func TestStreamIDValidation(t *testing.T) { testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ - Name: "stream_id_validation_test", + Name: "stream_id_validation_test", SeedStatements: migrations.GetSeedScriptStatements(), FunctionTests: []kwilTesting.TestFunc{ func(ctx context.Context, platform *kwilTesting.Platform) error { @@ -221,7 +242,7 @@ func TestStreamIDValidation(t *testing.T) { // TestAnyUserCanCreateStream tests that any user with a valid Ethereum address can create a stream func TestAnyUserCanCreateStream(t *testing.T) { testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ - Name: "any_user_can_create_stream_test", + Name: "any_user_can_create_stream_test", SeedStatements: migrations.GetSeedScriptStatements(), FunctionTests: []kwilTesting.TestFunc{ testAnyUserCanCreateStream(t), @@ -272,7 +293,7 @@ func testAnyUserCanCreateStream(t *testing.T) func(ctx context.Context, platform // TestMultipleStreamCreation tests that multiple streams can be created in a single transaction using CreateStreams func TestMultipleStreamCreation(t *testing.T) { testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ - Name: "multiple_stream_creation_test", + Name: "multiple_stream_creation_test", SeedStatements: migrations.GetSeedScriptStatements(), FunctionTests: []kwilTesting.TestFunc{ testMultipleStreamCreation(t),