@1money/protocol-ts-sdk 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ # API reference — read endpoints & response shapes
2
+
3
+ Every method below returns the SDK promise wrapper (see
4
+ `client-and-errors.md`). The value handed to `.success(cb)` / resolved by
5
+ `await` is the **decoded response body directly** — the shapes shown here are
6
+ exactly what you get.
7
+
8
+ All addresses, hashes, and large numbers are hex/decimal **strings**. `U256`
9
+ amounts are decimal strings (base units); `B256`/address values are `0x…` hex.
10
+
11
+ ## Table of contents
12
+ - [Client construction](#client-construction)
13
+ - [chain](#chain)
14
+ - [accounts](#accounts)
15
+ - [tokens (read)](#tokens-read)
16
+ - [transactions (read)](#transactions-read)
17
+ - [checkpoints](#checkpoints)
18
+ - [Write endpoints (index)](#write-endpoints-index)
19
+ - [Constants](#constants)
20
+
21
+ ## Client construction
22
+
23
+ ```typescript
24
+ import { api } from '@1money/protocol-ts-sdk';
25
+
26
+ const client = api(options?: {
27
+ network?: 'testnet' | 'mainnet' | 'local'; // default 'mainnet'
28
+ timeout?: number; // ms, default 10000
29
+ });
30
+ // → { accounts, checkpoints, tokens, transactions, chain }
31
+ ```
32
+
33
+ Base URLs by network: mainnet `https://api.1money.network`, testnet
34
+ `https://api.testnet.1money.network`, local `http://localhost:18555`.
35
+
36
+ ## chain
37
+
38
+ ```typescript
39
+ client.chain.getChainId() // → { chain_id: number }
40
+ ```
41
+
42
+ ## accounts
43
+
44
+ ```typescript
45
+ client.accounts.getNonce(address: string)
46
+ // → { nonce: number } next nonce to use for this sender
47
+
48
+ client.accounts.getBbNonce(address: string)
49
+ // → { bbnonce: number }
50
+
51
+ client.accounts.getTokenAccount(address: string, token: string)
52
+ // → { balance: string /* U256 base units */, nonce: number }
53
+ ```
54
+
55
+ `getTokenAccount` is how you read a wallet's balance of a specific token. To
56
+ compute the associated token-account address offline, use `deriveTokenAddress`
57
+ (see `client-and-errors.md` → Utilities).
58
+
59
+ ## tokens (read)
60
+
61
+ ```typescript
62
+ client.tokens.getTokenMetadata(token: string) // → MintInfo
63
+ ```
64
+
65
+ `MintInfo` (token-wide config and authorities):
66
+
67
+ ```typescript
68
+ interface MintInfo {
69
+ symbol: string;
70
+ master_authority: string;
71
+ master_mint_burn_authority: string;
72
+ mint_burn_authorities: { minter: string; allowance: string }[];
73
+ pause_authorities: string[];
74
+ list_authorities: string[];
75
+ black_list: string[];
76
+ white_list: string[];
77
+ metadata_update_authorities: string[];
78
+ bridge_mint_authorities: string[];
79
+ supply: string; // U256 base units
80
+ decimals: number;
81
+ is_paused: boolean;
82
+ is_private: boolean;
83
+ clawback_enabled: boolean;
84
+ meta: {
85
+ name: string;
86
+ uri: string;
87
+ additional_metadata: { key: string; value: string }[];
88
+ };
89
+ }
90
+ ```
91
+
92
+ ## transactions (read)
93
+
94
+ ```typescript
95
+ client.transactions.getByHash(hash: string) // → Transaction
96
+ client.transactions.getReceiptByHash(hash: string) // → TransactionReceipt
97
+ client.transactions.getFinalizedByHash(hash: string) // → FinalizedTransactionReceipt
98
+ client.transactions.estimateFee(from, to, value, token) // → { fee: string }
99
+ ```
100
+
101
+ `estimateFee(from: string, to: string, value: string, token: string)` — note the
102
+ argument order is **from, to, value, token** (value before to in the URL, but the
103
+ function signature is from/to/value/token).
104
+
105
+ `TransactionReceipt`:
106
+
107
+ ```typescript
108
+ interface TransactionReceipt {
109
+ success: boolean; // did the tx succeed on-chain
110
+ transaction_hash: string;
111
+ fee_used: number;
112
+ from: string;
113
+ checkpoint_hash?: string;
114
+ checkpoint_number?: number;
115
+ to?: string;
116
+ token_address?: string;
117
+ }
118
+ // FinalizedTransactionReceipt extends it with:
119
+ // epoch: number; counter_signatures: { r; s; v }[]
120
+ ```
121
+
122
+ `Transaction` is a **discriminated union** keyed by `transaction_type`
123
+ (`'TokenCreate' | 'TokenTransfer' | 'TokenMint' | 'TokenGrantAuthority' |
124
+ 'TokenRevokeAuthority' | 'TokenBlacklistAccount' | 'TokenWhitelistAccount' |
125
+ 'TokenBridgeAndMint' | 'TokenBurn' | 'TokenBurnAndBridge' | 'TokenClawback' |
126
+ 'TokenCloseAccount' | 'TokenPause' | 'TokenUnpause' | 'TokenUpdateMetadata' |
127
+ 'Raw'`). All variants share `hash`, `chain_id`, `from`, `nonce`, `signature`,
128
+ plus optional `checkpoint_*`/`transaction_index` and an optional `memo` (present
129
+ only for V2/memo-bearing txs); each carries a `data` object specific to its type.
130
+ Narrow on `transaction_type` before reading `data`.
131
+
132
+ ## checkpoints
133
+
134
+ (1Money's term for blocks.)
135
+
136
+ ```typescript
137
+ client.checkpoints.getNumber() // → { number: number }
138
+ client.checkpoints.getByHash(hash, full = false) // → Checkpoint
139
+ client.checkpoints.getByNumber(number, full = false) // number: number | string → Checkpoint
140
+ client.checkpoints.getReceiptsByNumber(number: number | string) // → TransactionReceipt[]
141
+ ```
142
+
143
+ `full` controls whether `transactions` comes back as full `Transaction[]`
144
+ (`true`) or just an array of hashes (`false`, default).
145
+
146
+ ```typescript
147
+ interface Checkpoint {
148
+ hash: string; parent_hash: string;
149
+ state_root: string; transactions_root: string; receipts_root: string;
150
+ number: number; timestamp: number;
151
+ size?: number;
152
+ transactions: Transaction[] | string[]; // hashes unless full=true
153
+ }
154
+ ```
155
+
156
+ ## Write endpoints (index)
157
+
158
+ These take a **signed payload** built via `TransactionBuilder` — see
159
+ `transactions.md`. Listed here only so you pick the right one.
160
+
161
+ ```typescript
162
+ client.transactions.payment(payload) // → { hash }
163
+ client.tokens.issueToken(payload) // → { hash, token }
164
+ client.tokens.mintToken(payload) // → { hash }
165
+ client.tokens.burnToken(payload) // → { hash }
166
+ client.tokens.grantAuthority(payload) // → { hash }
167
+ client.tokens.manageBlacklist(payload) // → { hash }
168
+ client.tokens.manageWhitelist(payload) // → { hash }
169
+ client.tokens.pauseToken(payload) // → { hash }
170
+ client.tokens.updateMetadata(payload) // → { hash }
171
+ client.tokens.bridgeAndMint(payload) // → { hash }
172
+ client.tokens.burnAndBridge(payload) // → { hash }
173
+ client.tokens.clawbackToken(payload) // → { hash }
174
+ ```
175
+
176
+ ## Constants
177
+
178
+ The chain ids are defined internally as `CHAIN_IDS` (mainnet `21210`, testnet
179
+ `1212101`, local `1212101`) but are **not re-exported** from any public entry
180
+ point (`.`, `/api`, `/client`, `/utils`). Don't try to import them — fetch the
181
+ live value at runtime:
182
+
183
+ ```typescript
184
+ const { chain_id } = await client.chain.getChainId();
185
+ ```
186
+
187
+ If you genuinely need a hardcoded id for an offline/test path, define your own
188
+ constant rather than relying on an unexported one.
@@ -0,0 +1,176 @@
1
+ # Client, error handling, config & utilities
2
+
3
+ ## The promise wrapper in depth
4
+
5
+ Every API method returns an object that is **both** a `Promise` and a builder of
6
+ handler chains. You consume it one of two ways.
7
+
8
+ ### Await style
9
+
10
+ ```typescript
11
+ try {
12
+ const res = await client.accounts.getNonce(addr); // resolves with the body
13
+ console.log(res.nonce);
14
+ } catch (err) {
15
+ // err is a ParsedError (see below) — thrown only when NO error handler chained
16
+ }
17
+ ```
18
+
19
+ ### Chain style
20
+
21
+ ```typescript
22
+ client.accounts.getNonce(addr)
23
+ .success(res => res.nonce) // res is the decoded body, not wrapped
24
+ .timeout(err => { /* fired specifically on configured timeout */ })
25
+ .error(err => { /* any other error */ });
26
+ ```
27
+
28
+ Handlers and what they fire on:
29
+
30
+ | Handler | Fires when |
31
+ | --- | --- |
32
+ | `.success(cb)` | request succeeded (HTTP 200 by `api()`'s rule) |
33
+ | `.failure(cb)` | a 2xx response that fails the success rule (rare — see note) |
34
+ | `.error(cb)` | network error, non-2xx HTTP status, or a throw in another handler |
35
+ | `.timeout(cb)` | the configured `timeout` elapsed (request is aborted) |
36
+ | `.login(cb)` | auth-required responses (HTTP 401 / `code === 401`) |
37
+ | `.rest(cb, scope?)` | catch-all for whichever cases you didn't handle |
38
+
39
+ > `api()` sets the success rule to `status === 200`. Since axios rejects non-2xx
40
+ > responses by default, most server-side failures (4xx/5xx) arrive at `.error`,
41
+ > not `.failure`. `.failure` only fires for a resolved response that isn't 200
42
+ > and isn't a login — uncommon in practice. For consumer code, handling
43
+ > `.success` + `.error` (or `try/catch`) covers the real cases.
44
+
45
+ Important behavioral notes:
46
+
47
+ - **Return value of the chain.** Awaiting with no handlers returns the raw body
48
+ (e.g. `await client.chain.getChainId()` → `{ chain_id }`) — this is the cleanest
49
+ idiom for the common case. Attaching `.success(cb)` instead makes the awaited
50
+ value equal to `cb`'s return, which is useful when you want to transform or map
51
+ the body inline.
52
+ - **Errors resolve vs. throw.** If you attach `.error()` (or `.rest()` covering
53
+ errors), the awaited chain **resolves** with the handler's return instead of
54
+ throwing. With no error handler, `await` **throws**. Don't half-mix: either
55
+ handle errors in the chain, or `try/catch` the await — not an awkward both.
56
+ - **`.rest(cb, scope)`** lets you target a subset, e.g. handle only timeout +
57
+ error with one callback: `.rest(cb, ['timeout', 'error'])`. An empty scope, or
58
+ a scope whose cases were all already handled, warns and never fires.
59
+
60
+ ### ParsedError shape
61
+
62
+ ```typescript
63
+ interface ParsedError<T extends string = string> {
64
+ name: T; // underlying error name (see timeout caveat below)
65
+ message: string;
66
+ stack: string;
67
+ status: number; // HTTP status, or 500 if none
68
+ data?: any; // server error body when present
69
+ }
70
+ ```
71
+
72
+ > **Timeout discriminator caveat.** A timeout is typed `ParsedError<'timeout'>`,
73
+ > but that `'timeout'` is only the *type-level* tag — at runtime the object is a
74
+ > plain `Error('timeout')`, so `name === 'Error'` and **`message === 'timeout'`**.
75
+ > To detect a timeout in a `catch`, check `err.message === 'timeout'` (or just use
76
+ > the `.timeout()` handler), **not** `err.name === 'timeout'`.
77
+
78
+ ## Configuration & custom headers
79
+
80
+ `api()` sets base URL, timeout, and the success rule globally. To add headers
81
+ (auth tokens, API keys) or override the base URL, use `setInitConfig` — headers
82
+ merge with the SDK's defaults rather than replacing them:
83
+
84
+ ```typescript
85
+ // Import from the /client subpath — setInitConfig is NOT a named export of the
86
+ // package root (the README's root import is inaccurate). The default `client`
87
+ // object from the root also carries it as `client.setInitConfig(...)`.
88
+ import { setInitConfig } from '@1money/protocol-ts-sdk/client';
89
+
90
+ setInitConfig({
91
+ headers: { Authorization: 'Bearer <token>', 'X-API-Key': '<key>' },
92
+ // optional:
93
+ baseURL: 'https://api.custom-domain.com',
94
+ timeout: 10000,
95
+ });
96
+ ```
97
+
98
+ > Config is **global/singleton** — it applies to all module references obtained
99
+ > from `api()`. Call `setInitConfig` once at startup (after `api()`), not
100
+ > per-request. There is no per-call header argument in the public methods.
101
+
102
+ ## Timeouts & cancellation
103
+
104
+ Default timeout is 10s; override per client via `api({ timeout })` or globally
105
+ via `setInitConfig({ timeout })`. On timeout the request is **aborted** (via
106
+ `AbortController`) and `.timeout()` fires (or the awaited call throws a
107
+ `ParsedError<'timeout'>` if unhandled — runtime `message === 'timeout'`, see the
108
+ caveat under ParsedError shape).
109
+
110
+ The high-level module methods (`accounts.getNonce`, `transactions.payment`, …)
111
+ do **not** accept a per-call options/`signal` argument — they use fixed request
112
+ options internally. Caller-supplied cancellation is only available through the
113
+ low-level `get`/`post` exports from `@1money/protocol-ts-sdk/client`, which do
114
+ accept an options object with a `signal`; that signal is merged with the SDK's
115
+ internal timeout abort signal.
116
+
117
+ ## Utilities
118
+
119
+ Imported from the package root (`@1money/protocol-ts-sdk`).
120
+
121
+ ### deriveTokenAddress(walletAddress, mintAddress)
122
+ Compute a wallet's associated token-account address offline (keccak-based
123
+ derivation) — no network call.
124
+
125
+ ```typescript
126
+ import { deriveTokenAddress } from '@1money/protocol-ts-sdk';
127
+ const tokenAccount = deriveTokenAddress(
128
+ '0xA634dfba8c7550550817898bC4820cD10888Aac5', // wallet
129
+ '0x8E9d1b45293e30EF38564582979195DD16A16E13', // mint
130
+ ); // → '0x…'
131
+ ```
132
+
133
+ ### toHex(value)
134
+ Convert booleans, numbers/bigints, integer strings, byte arrays, or arbitrary
135
+ values to a `0x…` hex string.
136
+
137
+ ```typescript
138
+ import { toHex } from '@1money/protocol-ts-sdk';
139
+ toHex(true); // '0x1' (minimal hex, not zero-padded — README's '0x01' is wrong)
140
+ toHex(123); // '0x7b'
141
+ toHex('hello'); // '0x68656c6c6f'
142
+ ```
143
+
144
+ ### calcTxHash(payload, signature)
145
+ Compute a transaction hash from a raw payload array + `{ r, s, v }`. Rarely
146
+ needed directly — prefer `SignedTx.txHash` from the builder flow, which is
147
+ computed for you. Use `calcTxHash` only for low-level/verification work.
148
+
149
+ ### validateMemo / MemoValidationError / Memo
150
+ For attaching a memo to a transaction (see `transactions.md` → Memo). `Memo` is
151
+ `{ type?, format?, data? }`. The builders call `validateMemo` for you when a memo
152
+ is present; call it yourself to pre-validate user input. It throws
153
+ `MemoValidationError` (with a `.code`) on oversize or illegal-character subfields.
154
+
155
+ ```typescript
156
+ import { validateMemo, MemoValidationError } from '@1money/protocol-ts-sdk';
157
+ try {
158
+ validateMemo({ type: 'invoice', data: userInput });
159
+ } catch (e) {
160
+ if (e instanceof MemoValidationError) console.error(e.code, e.message);
161
+ }
162
+ ```
163
+
164
+ ### Deprecated: signMessage / encodePayload
165
+ `signMessage(payload, privateKey)` and `encodePayload(payload)` are legacy
166
+ `@deprecated` helpers retained for backward compatibility. **Do not use them for
167
+ new code** — `TransactionBuilder` + `createPrivateKeySigner` (see
168
+ `transactions.md`) is the supported, validated, malleability-safe path.
169
+
170
+ ## Security checklist
171
+
172
+ - Load private keys from environment/secret managers; never hardcode or commit
173
+ them. Example value strings in docs are placeholders.
174
+ - Use `testnet` (or `local`) while developing; switch the network deliberately.
175
+ - Keep peer deps (`axios`, `viem`, `@ethereumjs/rlp`) within the package's
176
+ declared ranges; mismatches can break signing/encoding subtly.
@@ -0,0 +1,325 @@
1
+ # Building, signing & submitting transactions
2
+
3
+ Every write follows the same pipeline. Internalize this shape; the rest is just
4
+ filling in builder-specific fields.
5
+
6
+ ```typescript
7
+ import { api, TransactionBuilder, createPrivateKeySigner } from '@1money/protocol-ts-sdk';
8
+
9
+ const client = api({ network: 'testnet' });
10
+
11
+ // 1. Inputs every tx needs. Awaiting directly returns the body; it throws on
12
+ // error (or attach .success/.error handlers — see client-and-errors.md).
13
+ const { chain_id } = await client.chain.getChainId();
14
+ const { nonce } = await client.accounts.getNonce(sender);
15
+
16
+ // 2. Build (validates fields, RLP-encodes, prepares the digest).
17
+ const prepared = TransactionBuilder.<type>({ chain_id, nonce, /* ...fields */ });
18
+
19
+ // 3. Sign → request body → submit to the matching endpoint.
20
+ const signed = await prepared.sign(createPrivateKeySigner(privateKey));
21
+ const res = await client.<module>.<method>(signed.toRequest());
22
+ ```
23
+
24
+ ## What the objects give you
25
+
26
+ `TransactionBuilder.<type>(...)` returns a **PreparedTx**:
27
+ - `prepared.signatureHash` — the 32-byte digest a signer must sign.
28
+ - `prepared.rlpBytes`, `prepared.unsigned`, `prepared.kind`.
29
+ - `await prepared.sign(signer)` → **SignedTx**.
30
+ - `prepared.attachSignature(signature)` → **SignedTx** (when you signed the
31
+ `signatureHash` elsewhere and just want to attach r/s/v).
32
+
33
+ `SignedTx`:
34
+ - `signed.toRequest()` — the request body (unsigned fields + `signature`) you
35
+ pass to the submit method.
36
+ - `signed.txHash` — the on-chain transaction hash (available before submit).
37
+ - `signed.signature` (`{ r, s, v }`), `signed.signatureHash`, `signed.unsigned`.
38
+
39
+ `createPrivateKeySigner(privateKey: \`0x${string}\`)` produces a `SignerAdapter`
40
+ that signs with low-S (required — high-S signatures are rejected on attach).
41
+
42
+ ## Builder ↔ endpoint map
43
+
44
+ | `TransactionBuilder.…` | Submit `client.…` | Returns |
45
+ | --- | --- | --- |
46
+ | `payment` | `transactions.payment` | `{ hash }` |
47
+ | `tokenIssue` | `tokens.issueToken` | `{ hash, token }` |
48
+ | `tokenMint` | `tokens.mintToken` | `{ hash }` |
49
+ | `tokenBurn` | `tokens.burnToken` | `{ hash }` |
50
+ | `tokenAuthority` | `tokens.grantAuthority` | `{ hash }` |
51
+ | `tokenManageList` | `tokens.manageBlacklist` / `tokens.manageWhitelist` | `{ hash }` |
52
+ | `tokenPause` | `tokens.pauseToken` | `{ hash }` |
53
+ | `tokenMetadata` | `tokens.updateMetadata` | `{ hash }` |
54
+ | `tokenBridgeAndMint` | `tokens.bridgeAndMint` | `{ hash }` |
55
+ | `tokenBurnAndBridge` | `tokens.burnAndBridge` | `{ hash }` |
56
+ | `tokenClawback` | `tokens.clawbackToken` | `{ hash }` |
57
+
58
+ ## Builder fields
59
+
60
+ All builders take `chain_id: number` and `nonce: number`. Fields below are the
61
+ *additional* ones. `value`/amount fields are **decimal strings in base units**.
62
+ Addresses are EIP-55 `0x…` strings.
63
+
64
+ **Every builder also accepts an optional `memo?: Memo`** (see [Memo](#memo-optional--v1-vs-v2-envelope) below). Omit it for the common case; the fields listed per builder are the required ones.
65
+
66
+ ### payment → transactions.payment
67
+ ```typescript
68
+ { recipient: string; value: string; token: string }
69
+ ```
70
+
71
+ ### tokenIssue → tokens.issueToken
72
+ ```typescript
73
+ {
74
+ symbol: string;
75
+ name: string;
76
+ decimals: number;
77
+ master_authority: string;
78
+ is_private: boolean;
79
+ clawback_enabled?: boolean; // default true
80
+ }
81
+ // response includes the new token's address: { hash, token }
82
+ ```
83
+
84
+ ### tokenMint → tokens.mintToken
85
+ ```typescript
86
+ { recipient: string; value: string; token: string }
87
+ ```
88
+
89
+ ### tokenBurn → tokens.burnToken
90
+ ```typescript
91
+ { value: string; token: string }
92
+ ```
93
+
94
+ ### tokenAuthority → tokens.grantAuthority
95
+ ```typescript
96
+ import { AuthorityAction, AuthorityType } from '@1money/protocol-ts-sdk/api';
97
+ {
98
+ action: AuthorityAction; // Grant | Revoke
99
+ authority_type: AuthorityType; // see enum below
100
+ authority_address: string;
101
+ token: string;
102
+ value?: string; // e.g. mint/burn allowance
103
+ }
104
+ ```
105
+
106
+ ### tokenManageList → tokens.manageBlacklist / tokens.manageWhitelist
107
+ ```typescript
108
+ import { ManageListAction } from '@1money/protocol-ts-sdk/api';
109
+ { action: ManageListAction; address: string; token: string } // Add | Remove
110
+ ```
111
+ Build once, then submit to `manageBlacklist` *or* `manageWhitelist` — the payload
112
+ shape is identical; the endpoint decides which list.
113
+
114
+ ### tokenPause → tokens.pauseToken
115
+ ```typescript
116
+ import { PauseAction } from '@1money/protocol-ts-sdk/api';
117
+ { action: PauseAction; token: string } // Pause | Unpause
118
+ ```
119
+
120
+ ### tokenMetadata → tokens.updateMetadata
121
+ ```typescript
122
+ {
123
+ name: string;
124
+ uri: string;
125
+ token: string;
126
+ additional_metadata: { key: string; value: string }[];
127
+ }
128
+ ```
129
+
130
+ ### tokenBridgeAndMint → tokens.bridgeAndMint
131
+ ```typescript
132
+ {
133
+ recipient: string;
134
+ value: string;
135
+ token: string;
136
+ source_chain_id: number;
137
+ source_tx_hash: string;
138
+ bridge_metadata: string;
139
+ }
140
+ ```
141
+
142
+ ### tokenBurnAndBridge → tokens.burnAndBridge
143
+ ```typescript
144
+ {
145
+ sender: string;
146
+ value: string;
147
+ token: string;
148
+ destination_chain_id: number;
149
+ destination_address: string;
150
+ escrow_fee: string;
151
+ bridge_metadata: string;
152
+ bridge_param: string; // bytes as 0x… hex ('0x' for empty)
153
+ }
154
+ ```
155
+
156
+ ### tokenClawback → tokens.clawbackToken
157
+ ```typescript
158
+ { token: string; from: string; recipient: string; value: string }
159
+ ```
160
+
161
+ ## Memo (optional — V1 vs V2 envelope)
162
+
163
+ Any builder accepts an optional `memo` to attach a structured note. `Memo` is
164
+ imported from the package root (or `/utils`); all three subfields are optional:
165
+
166
+ ```typescript
167
+ import type { Memo } from '@1money/protocol-ts-sdk';
168
+
169
+ const prepared = TransactionBuilder.payment({
170
+ chain_id, nonce, recipient, value, token,
171
+ memo: { type: 'invoice', format: 'text', data: 'order-12345' }, // all subfields optional
172
+ });
173
+ ```
174
+
175
+ What it changes — this is **not cosmetic**:
176
+
177
+ - **Absent memo (`undefined`/`null`) → V1 path.** RLP bytes are byte-identical to
178
+ the pre-memo SDK; `prepared.kind` is e.g. `'payment'`.
179
+ - **Present memo (even `{}` with empty subfields) → V2 envelope.** The builder
180
+ RLP-encodes `[innerList, [type, format, data]]`, `prepared.kind` becomes
181
+ `'<kind>_v2'`, and the **signature hash and tx hash live in a different domain**
182
+ — a V1 and a V2 tx with otherwise identical fields produce different hashes.
183
+ So passing `memo: {}` is a meaningful choice, not a no-op.
184
+
185
+ Validation runs at build time (mirrors the server's Rust rules) and throws
186
+ `MemoValidationError` (carries a `.code`) on violation:
187
+
188
+ | Field | Cap | Allowed chars |
189
+ | --- | --- | --- |
190
+ | `type` | 128 bytes (UTF-8) | URL-safe (RFC 3986 unreserved + gen/sub-delims + `%`) |
191
+ | `format` | 64 bytes (UTF-8) | URL-safe (same set) |
192
+ | `data` | 256 bytes (UTF-8) | any non-control: rejects NUL, C0/C1 controls, surrogates |
193
+
194
+ Error codes: `MEMO_TYPE_TOO_LONG`, `MEMO_TYPE_INVALID_CHARS`,
195
+ `MEMO_FORMAT_TOO_LONG`, `MEMO_FORMAT_INVALID_CHARS`, `MEMO_DATA_TOO_LONG`,
196
+ `MEMO_DATA_CONTROL_CHARS`, `MEMO_TOO_LARGE`. On the read side, the `Transaction`
197
+ union carries `memo?` back (populated only for V2 transactions); the receipt
198
+ types do not include it.
199
+
200
+ ## Enums (import from `@1money/protocol-ts-sdk/api`)
201
+
202
+ ```typescript
203
+ enum AuthorityType {
204
+ MasterMint = 'MasterMintBurn',
205
+ MintBurnTokens = 'MintBurnTokens',
206
+ Pause = 'Pause',
207
+ ManageList = 'ManageList',
208
+ UpdateMetadata = 'UpdateMetadata',
209
+ Bridge = 'Bridge',
210
+ Clawback = 'Clawback',
211
+ }
212
+ enum AuthorityAction { Grant = 'Grant', Revoke = 'Revoke' }
213
+ enum ManageListAction { Add = 'Add', Remove = 'Remove' }
214
+ enum PauseAction { Pause = 'Pause', Unpause = 'Unpause' }
215
+ ```
216
+
217
+ ## Worked example: issue a token, then read its address
218
+
219
+ ```typescript
220
+ import { api, TransactionBuilder, createPrivateKeySigner } from '@1money/protocol-ts-sdk';
221
+
222
+ const client = api({ network: 'testnet' });
223
+ const master = '0x9E1E9688A44D058fF181Ed64ddFAFbBE5CC74ff3';
224
+ const privateKey = process.env.ONE_MONEY_PRIVATE_KEY as `0x${string}`;
225
+
226
+ const { chain_id } = await client.chain.getChainId();
227
+ const { nonce } = await client.accounts.getNonce(master);
228
+
229
+ const prepared = TransactionBuilder.tokenIssue({
230
+ chain_id, nonce,
231
+ symbol: 'MTK', name: 'My Token', decimals: 18,
232
+ master_authority: master,
233
+ is_private: true,
234
+ clawback_enabled: true,
235
+ });
236
+
237
+ const signed = await prepared.sign(createPrivateKeySigner(privateKey));
238
+ const { hash, token } = await client.tokens.issueToken(signed.toRequest());
239
+ console.log('issued token', token, 'in tx', hash);
240
+ ```
241
+
242
+ ## Custom signer (wallet / HSM / no raw key in process)
243
+
244
+ When the private key lives in a browser wallet, KMS, or HSM, implement the
245
+ `SignerAdapter` interface instead of `createPrivateKeySigner`. You only need to
246
+ sign `prepared.signatureHash` and return `{ r, s, v }` (low-S):
247
+
248
+ ```typescript
249
+ import type { SignerAdapter, Signature } from '@1money/protocol-ts-sdk';
250
+
251
+ const walletSigner: SignerAdapter = {
252
+ async signDigest(digest): Promise<Signature> {
253
+ // digest === prepared.signatureHash, a 0x-prefixed 32-byte hex string.
254
+ // Produce a low-S secp256k1 signature however your key custody allows.
255
+ return { r: '0x…', s: '0x…', v: 0 };
256
+ },
257
+ };
258
+
259
+ const signed = await prepared.sign(walletSigner);
260
+ // or, if you already have the signature object:
261
+ const signed2 = prepared.attachSignature({ r: '0x…', s: '0x…', v: 0 });
262
+ ```
263
+
264
+ `v` may be `number` (recovery id, e.g. `0`/`1`) or `boolean`. `attachSignature`
265
+ throws on high-S signatures (malleability guard), so ensure your signer enforces
266
+ low-S.
267
+
268
+ ## Alternate path: EIP-712 typed-data payment
269
+
270
+ Besides the RLP `TransactionBuilder` flow above, the SDK exports a **separate
271
+ EIP-712 typed-data path for payments** (root-level export, via the signing
272
+ layer). Use it when the signer is a browser wallet doing
273
+ `eth_signTypedData_v4` and you submit on-chain calldata to a `submitTypedData`
274
+ contract entrypoint — *not* the REST `transactions.payment` endpoint. Today only
275
+ payment is implemented.
276
+
277
+ ```typescript
278
+ import { preparePaymentTypedTx, parseSig } from '@1money/protocol-ts-sdk';
279
+
280
+ const prepared = preparePaymentTypedTx({
281
+ chain_id, nonce, recipient, value, token,
282
+ memo: { data: 'note' }, // optional, same Memo rules
283
+ });
284
+
285
+ // 1. Hand prepared.typedData to the wallet for eth_signTypedData_v4.
286
+ const sigHex = await wallet.request({
287
+ method: 'eth_signTypedData_v4',
288
+ params: [account, JSON.stringify(prepared.typedData)],
289
+ });
290
+
291
+ // 2. Parse the 65-byte sig (normalizes v to 27/28) and build the calldata.
292
+ const tx = prepared.encodeCalldata(parseSig(sigHex));
293
+ // → { to, data, value: 0n, gas, gasPrice, type: 'legacy' } — send via your wallet.
294
+ ```
295
+
296
+ `encodeCalldata` requires `v ∈ {27, 28}` (recovery ids `0`/`1` are normalized by
297
+ `parseSig`). The EIP-712 domain is `{ name: '1Money Network', version: '1',
298
+ chainId, verifyingContract: 0xff…fe }`; `buildPaymentEip712TypedData` is exported
299
+ if you need the typed data without the calldata helper. This path is independent
300
+ of the `api()` REST client.
301
+
302
+ ## Verifying the result
303
+
304
+ After submit you get a `{ hash }` (and `{ token }` for issue). Confirm it landed:
305
+
306
+ ```typescript
307
+ const receipt = await client.transactions.getReceiptByHash(hash);
308
+ console.log(receipt.success ? 'confirmed' : 'failed', 'fee:', receipt.fee_used);
309
+ ```
310
+
311
+ Receipts may not be available immediately — poll `getReceiptByHash` (or
312
+ `getFinalizedByHash` for finality) with a short backoff rather than a single
313
+ call.
314
+
315
+ ## Common mistakes
316
+
317
+ - **Wrong builder/endpoint pairing** — e.g. building `tokenMint` but calling
318
+ `tokens.issueToken`. Use the map above.
319
+ - **Numeric amounts** — `value: 1` or `value: 1.5` is wrong; use a base-unit
320
+ string like `'1000000000000000000'`.
321
+ - **Stale nonce across multiple txs** — re-fetch or locally increment `nonce`
322
+ between sequential sends from the same sender.
323
+ - **Forgetting `.toRequest()`** — you submit the SignedTx's request body, not the
324
+ PreparedTx or the SignedTx object itself.
325
+ - **Reaching for `signMessage`/`encodePayload`** — deprecated; use the builder.