The SDK, for an afternoon rather than a week
amw-finance-sdk is the same four acts the previous page
spells out against the ABI, with the EIP-712 types and the flag decoding already written.
Nothing here is new behaviour. Every call below is one the contracts already answer.
It depends on viem and @amw/fixtures, which is where every figure in this project
is stated once. No database, no indexer, no Hedera SDK, no server. Read the raw path first if
you want to know what it is doing; use this if you would rather not copy typed-data
definitions out of a test helper.
Installing it
npm install amw-finance-sdk viem
One bundled ESM file with the figures inlined, its declarations beside it, and viem left external, because an integrator almost certainly has one already and two copies of it in a process is two opinions about what an address is. Nothing else is named: no dependency in the manifest resolves only inside this workspace.
The workspace is still @amw/sdk and the package on the registry is
amw-finance-sdk, which is the deployment's own domain. Two refusals shaped that:
@amw is somebody else's scope, and npm rejects a bare amw-sdk as too
close to aws-sdk. npm run pack:sdk stages the tarball and six tests read that
staged tree rather than the script's report of it, because a package that installs and fails on
the first import is a defect discovered by whoever trusted this page.
Everything this page says about behaviour is exercised by
packages/sdk/src/e2e.test.ts, which drafts a mandate, signs it, registers it,
fires actions under it and reads the verdict through this package and nothing else.
The whole of it
import { draft, sign, register, request, deploymentFor, usdc } from "amw-finance-sdk";
const { MandateRegistry, MandateGuard } = deploymentFor(296);
// 1, the instrument, in the units a person writes.
const mandate = draft({
owner: owner.address, agent: agent.address,
tokenIn: USDC, tokenOut: EURC,
perAction: 250_000, // clause 2.1
perWindow: 2_000_000, // clause 3.4
actions: 12, // clause 3.5, at most 16
offPrimaryBps: 2500, // clause 3.7
minReturnBps: 9700, // clause 2.5, measured on what came back
windowMinutes: 60,
expiresInHours: 24, // clause 2.4
});
// 2, the owner signs. This is the whole of the authority.
const signature = await sign(wallet, owner, 296, MandateRegistry, mandate);
// 3: anyone may submit it. A relayer paying gas for an owner who holds
// none is still submitting that owner's mandate.
await register(wallet, submitter, MandateRegistry, mandate, signature);
// 4: the agent asks. It never executes: the guard decides, and the venue
// class and notional are derived inside it.
const verdict = await request({ chain, wallet }, MandateGuard, agent, {
mandateHash, target: venue, amountIn: usdc(240_000),
minReturnBps: 9700, data: swapCalldata,
});
if (verdict.held) console.log(verdict.tier, verdict.clauses); // "tier 2" ["3.4","3.7"]
A breach returns; it does not revert
This is the one thing to get right, and it is why request returns a
verdict rather than a receipt. A held action has an action id and an
IntentRecorded entry; treating it as a failure discards the record the product
exists to produce.
verdict.tier | What happened |
|---|---|
clean | Every clause satisfied. The action executed |
tier 1 | This action was not permitted: its cap, its pair, its venue, its expiry, its floor |
tier 2 | This action was fine and the sequence was not. The composition case |
both tiers | Both, which the guard reports together rather than stopping at the first |
verdict.clauses gives what a supervisor cites, ["3.4", "3.7"], and
verdict.names the bits the contract raised.
Never treat a hold as a transport failure. Retrying it is how an agent turns one refusal into a queue of refusals. The run ends where a supervisor’s work begins.
draft refuses a mandate the registry would
It throws, naming the clause, rather than handing back a struct that fails three steps later as a revert against a signature somebody has already given:
draft({ ...terms, actions: 20 });
// Error: this mandate cannot be registered: clause 3.5 admits at most 16
// actions: the window is summed exactly, and the ledger it is summed
// from is what bounds it
Four bounds are checked, and they are the registry’s own rather than a copy: at most sixteen actions to a window, a clause 2.5 floor no higher than the whole, a window of more than zero seconds, and two legs that are not the same token. The console’s mandate form calls the same function before it asks for a signature, so a person and an integrator meet the same refusal in the same words.
What it does not do
- Custody. The guard is the sole approved spender; approving it is an ordinary ERC-20
approve, andTOKEN_ABIis here for that and nothing more. - Resolution. A hold is ended by a named authoriser signing from their own key.
GUARD_ABIcarriesresolveandresolveWithSig; deciding who may call them is the owner’s, on chain, and no helper here will paper over it. - Evidence. The intent log, the commitments and the case files belong to the spine, and the spine is rebuildable from chain. To verify a disclosed record,
@amw/intent-log/digestrecomputes the digest and /console/verify does it in a browser without asking AMW anything.
Addresses, committed rather than fetched
An integrator reading an address out of a running service is trusting that service to tell them
which contracts they are bound by, and that is the one thing they should not have to trust. So
deploymentFor(296) returns addresses compiled into the package, and refuses an
unknown chain by name rather than returning undefined into a call that fails later.
The cost of committing them is that a redeployment can leave them behind, and once did: the package named the previous guard for a day after the chain had moved, which is an integrator calling a contract nobody enforces against any more. A test now pins them to the file the deploy script writes.
Proving it
Unit tests say the ABIs and the flag decoding are right; they cannot say the package works.
e2e.test.ts does the whole of an integrator’s job through this package and nothing
else, draft, sign, register, fire, read the verdict, against a simulated chain, and asserts
the product’s own claim through it: two actions execute, the third is held on clause 3.4 with
every per-action bound satisfied, and the window afterwards shows the hold debited nothing.
npx hardhat node # in packages/contracts
npm run -w @amw/sdk test
Without a node it skips and says so: read the skip count, not only the pass count. It never writes to the deployed guard: that contract has no upgrade path and its recorded incident is what a reader is shown, so a test that wrote to it would be spending the demonstration to prove a helper.
The addresses in deploymentFor are the ones this repository deployed and verified,
so the version an integrator installs and the contracts this documentation describes are the
same pair.