@ar.io/sdk 3.24.0 → 4.0.0-alpha.2

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.
Files changed (169) hide show
  1. package/README.md +757 -589
  2. package/lib/esm/cli/cli.js +188 -152
  3. package/lib/esm/cli/commands/antCommands.js +23 -58
  4. package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
  5. package/lib/esm/cli/commands/escrowCommands.js +227 -0
  6. package/lib/esm/cli/commands/gatewayWriteCommands.js +140 -23
  7. package/lib/esm/cli/commands/pruneCommands.js +154 -0
  8. package/lib/esm/cli/commands/readCommands.js +22 -3
  9. package/lib/esm/cli/commands/transfer.js +6 -6
  10. package/lib/esm/cli/options.js +124 -58
  11. package/lib/esm/cli/utils.js +303 -175
  12. package/lib/esm/common/ant-registry.js +17 -143
  13. package/lib/esm/common/ant.js +44 -1167
  14. package/lib/esm/common/faucet.js +17 -6
  15. package/lib/esm/common/index.js +0 -4
  16. package/lib/esm/common/io.js +25 -1412
  17. package/lib/esm/constants.js +13 -19
  18. package/lib/esm/solana/ant-readable.js +724 -0
  19. package/lib/esm/solana/ant-registry-readable.js +133 -0
  20. package/lib/esm/solana/ant-registry-writeable.js +472 -0
  21. package/lib/esm/solana/ant-writeable.js +384 -0
  22. package/lib/esm/solana/ata.js +70 -0
  23. package/lib/esm/solana/canonical-message.js +128 -0
  24. package/lib/esm/solana/clusters.js +111 -0
  25. package/lib/esm/solana/constants.js +146 -0
  26. package/lib/esm/solana/delegation-math.js +112 -0
  27. package/lib/esm/solana/deserialize.js +711 -0
  28. package/lib/esm/solana/escrow.js +839 -0
  29. package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
  30. package/lib/esm/solana/funding-plan.js +699 -0
  31. package/lib/esm/solana/index.js +126 -0
  32. package/lib/esm/solana/instruction.js +39 -0
  33. package/lib/esm/solana/io-readable.js +2182 -0
  34. package/lib/esm/solana/io-writeable.js +3196 -0
  35. package/lib/esm/solana/json-rpc.js +90 -0
  36. package/lib/esm/solana/metadata.js +81 -0
  37. package/lib/esm/solana/mpl-core.js +192 -0
  38. package/lib/esm/solana/pda.js +332 -0
  39. package/lib/esm/solana/predict-prescribed-observers.js +110 -0
  40. package/lib/esm/solana/retry.js +117 -0
  41. package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
  42. package/lib/esm/solana/send.js +372 -0
  43. package/lib/esm/solana/spawn-ant.js +224 -0
  44. package/lib/esm/solana/types.js +1 -0
  45. package/lib/esm/types/ant.js +27 -15
  46. package/lib/esm/types/io.js +8 -11
  47. package/lib/esm/utils/ant.js +0 -63
  48. package/lib/esm/utils/index.js +0 -3
  49. package/lib/esm/version.js +1 -1
  50. package/lib/types/cli/commands/antCommands.d.ts +5 -13
  51. package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
  52. package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
  53. package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
  54. package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
  55. package/lib/types/cli/commands/readCommands.d.ts +27 -22
  56. package/lib/types/cli/commands/transfer.d.ts +9 -9
  57. package/lib/types/cli/options.d.ts +76 -21
  58. package/lib/types/cli/types.d.ts +11 -13
  59. package/lib/types/cli/utils.d.ts +71 -31
  60. package/lib/types/common/ant-registry.d.ts +49 -47
  61. package/lib/types/common/ant.d.ts +54 -539
  62. package/lib/types/common/faucet.d.ts +20 -8
  63. package/lib/types/common/index.d.ts +0 -3
  64. package/lib/types/common/io.d.ts +66 -258
  65. package/lib/types/constants.d.ts +11 -18
  66. package/lib/types/solana/ant-readable.d.ts +180 -0
  67. package/lib/types/solana/ant-registry-readable.d.ts +105 -0
  68. package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
  69. package/lib/types/solana/ant-writeable.d.ts +177 -0
  70. package/lib/types/solana/ata.d.ts +44 -0
  71. package/lib/types/solana/canonical-message.d.ts +121 -0
  72. package/lib/types/solana/clusters.d.ts +109 -0
  73. package/lib/types/solana/constants.d.ts +119 -0
  74. package/lib/types/solana/delegation-math.d.ts +45 -0
  75. package/lib/types/solana/deserialize.d.ts +262 -0
  76. package/lib/types/solana/escrow.d.ts +480 -0
  77. package/lib/types/solana/events.d.ts +38 -0
  78. package/lib/types/solana/funding-plan.d.ts +225 -0
  79. package/lib/types/solana/index.d.ts +87 -0
  80. package/lib/types/solana/instruction.d.ts +39 -0
  81. package/lib/types/solana/io-readable.d.ts +499 -0
  82. package/lib/types/solana/io-writeable.d.ts +893 -0
  83. package/lib/types/solana/json-rpc.d.ts +47 -0
  84. package/lib/types/solana/metadata.d.ts +84 -0
  85. package/lib/types/solana/mpl-core.d.ts +120 -0
  86. package/lib/types/solana/pda.d.ts +95 -0
  87. package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
  88. package/lib/types/solana/retry.d.ts +62 -0
  89. package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
  90. package/lib/types/solana/send.d.ts +94 -0
  91. package/lib/types/solana/spawn-ant.d.ts +145 -0
  92. package/lib/types/solana/types.d.ts +82 -0
  93. package/lib/types/types/ant-registry.d.ts +43 -4
  94. package/lib/types/types/ant.d.ts +114 -96
  95. package/lib/types/types/common.d.ts +18 -74
  96. package/lib/types/types/faucet.d.ts +2 -2
  97. package/lib/types/types/io.d.ts +244 -158
  98. package/lib/types/types/token.d.ts +0 -12
  99. package/lib/types/utils/ant.d.ts +1 -12
  100. package/lib/types/utils/index.d.ts +0 -3
  101. package/lib/types/version.d.ts +1 -1
  102. package/package.json +36 -33
  103. package/lib/cjs/cli/cli.js +0 -822
  104. package/lib/cjs/cli/commands/antCommands.js +0 -113
  105. package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
  106. package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
  107. package/lib/cjs/cli/commands/readCommands.js +0 -215
  108. package/lib/cjs/cli/commands/transfer.js +0 -159
  109. package/lib/cjs/cli/options.js +0 -470
  110. package/lib/cjs/cli/types.js +0 -2
  111. package/lib/cjs/cli/utils.js +0 -639
  112. package/lib/cjs/common/ant-registry.js +0 -155
  113. package/lib/cjs/common/ant-versions.js +0 -93
  114. package/lib/cjs/common/ant.js +0 -1182
  115. package/lib/cjs/common/arweave.js +0 -27
  116. package/lib/cjs/common/contracts/ao-process.js +0 -224
  117. package/lib/cjs/common/error.js +0 -64
  118. package/lib/cjs/common/faucet.js +0 -150
  119. package/lib/cjs/common/hyperbeam/hb.js +0 -173
  120. package/lib/cjs/common/index.js +0 -42
  121. package/lib/cjs/common/io.js +0 -1423
  122. package/lib/cjs/common/logger.js +0 -83
  123. package/lib/cjs/common/loggers/winston.js +0 -68
  124. package/lib/cjs/common/marketplace.js +0 -731
  125. package/lib/cjs/common/turbo.js +0 -223
  126. package/lib/cjs/constants.js +0 -41
  127. package/lib/cjs/node/index.js +0 -39
  128. package/lib/cjs/package.json +0 -1
  129. package/lib/cjs/types/ant-registry.js +0 -2
  130. package/lib/cjs/types/ant.js +0 -168
  131. package/lib/cjs/types/common.js +0 -2
  132. package/lib/cjs/types/faucet.js +0 -2
  133. package/lib/cjs/types/index.js +0 -37
  134. package/lib/cjs/types/io.js +0 -51
  135. package/lib/cjs/types/token.js +0 -116
  136. package/lib/cjs/utils/ant.js +0 -108
  137. package/lib/cjs/utils/ao.js +0 -432
  138. package/lib/cjs/utils/arweave.js +0 -285
  139. package/lib/cjs/utils/base64.js +0 -62
  140. package/lib/cjs/utils/hash.js +0 -56
  141. package/lib/cjs/utils/index.js +0 -38
  142. package/lib/cjs/utils/processes.js +0 -173
  143. package/lib/cjs/utils/random.js +0 -30
  144. package/lib/cjs/utils/schema.js +0 -15
  145. package/lib/cjs/utils/url.js +0 -37
  146. package/lib/cjs/version.js +0 -20
  147. package/lib/cjs/web/index.js +0 -41
  148. package/lib/esm/common/ant-versions.js +0 -87
  149. package/lib/esm/common/arweave.js +0 -21
  150. package/lib/esm/common/contracts/ao-process.js +0 -220
  151. package/lib/esm/common/hyperbeam/hb.js +0 -169
  152. package/lib/esm/common/marketplace.js +0 -724
  153. package/lib/esm/common/turbo.js +0 -215
  154. package/lib/esm/node/index.js +0 -20
  155. package/lib/esm/utils/ao.js +0 -420
  156. package/lib/esm/utils/arweave.js +0 -271
  157. package/lib/esm/utils/processes.js +0 -167
  158. package/lib/esm/web/index.js +0 -20
  159. package/lib/types/common/ant-versions.d.ts +0 -39
  160. package/lib/types/common/arweave.d.ts +0 -17
  161. package/lib/types/common/contracts/ao-process.d.ts +0 -47
  162. package/lib/types/common/hyperbeam/hb.d.ts +0 -88
  163. package/lib/types/common/marketplace.d.ts +0 -568
  164. package/lib/types/common/turbo.d.ts +0 -61
  165. package/lib/types/node/index.d.ts +0 -20
  166. package/lib/types/utils/ao.d.ts +0 -80
  167. package/lib/types/utils/arweave.d.ts +0 -79
  168. package/lib/types/utils/processes.d.ts +0 -39
  169. package/lib/types/web/index.d.ts +0 -20
@@ -0,0 +1,839 @@
1
+ /**
2
+ * Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Solana ANT-escrow and Token-escrow clients — `ario-ant-escrow` program.
18
+ *
19
+ * ANTEscrow holds a Metaplex Core ANT NFT in trustless custody and
20
+ * releases it after on-chain verification of an Arweave RSA-PSS-4096 or
21
+ * Ethereum ECDSA signature over a canonical claim message.
22
+ *
23
+ * TokenEscrow holds ARIO SPL tokens (liquid or vaulted) in trustless
24
+ * custody with the same multi-protocol claim flow.
25
+ *
26
+ * Design: `docs/ANT_ESCROW_DESIGN.md` (account model, canonical message
27
+ * format, threat model). Plan: `docs/ANT_ESCROW_IMPLEMENTATION_PLAN.md`.
28
+ *
29
+ * All instruction encoding is delegated to the Codama-generated builders
30
+ * in `./generated/ant-escrow/instructions/` — they own the discriminator,
31
+ * Borsh codec, and account-meta wiring derived from the on-chain IDL.
32
+ */
33
+ import { fetchMaybeEscrowAnt, fetchMaybeEscrowToken, getCancelDepositInstruction, getCancelTokenDepositInstruction, getCancelVaultDepositInstruction, getClaimAntArweaveAttestedInstruction, getClaimAntEthereumInstruction, getClaimTokensArweaveAttestedInstruction, getClaimTokensEthereumInstruction, getClaimVaultArweaveAttestedInstruction, getClaimVaultEthereumInstruction, getDepositAntInstruction, getDepositTokensInstruction, getDepositVaultInstruction, getUpdateRecipientInstruction, getUpdateTokenRecipientInstruction, getUpdateVaultRecipientInstruction, } from '@ar.io/solana-contracts/ant-escrow';
34
+ import { Logger } from '../common/logger.js';
35
+ import { getAssociatedTokenAddressKit } from './ata.js';
36
+ import { ARIO_ANT_ESCROW_PROGRAM_ID, ARIO_CORE_PROGRAM_ID, ESCROW_ARWEAVE_PUBKEY_LEN, ESCROW_ASSET_TYPE_VAULT, ESCROW_ETHEREUM_PUBKEY_LEN, ESCROW_PROTOCOL_ARWEAVE, ESCROW_PROTOCOL_ETHEREUM, } from './constants.js';
37
+ import { getEscrowAntPDA, getEscrowTokenPDA, getEscrowVaultPDA, } from './pda.js';
38
+ import { sendAndConfirm } from './send.js';
39
+ /** Map the Codama-generated `EscrowAnt` raw decoded type to our public
40
+ * `EscrowAntState` with protocol enum + active-prefix pubkey slice. */
41
+ function toEscrowAntState(raw) {
42
+ const recipientProtocol = raw.recipientProtocol === ESCROW_PROTOCOL_ARWEAVE ? 'arweave' : 'ethereum';
43
+ const expectedLen = recipientProtocol === 'arweave'
44
+ ? ESCROW_ARWEAVE_PUBKEY_LEN
45
+ : ESCROW_ETHEREUM_PUBKEY_LEN;
46
+ if (raw.recipientProtocol !== ESCROW_PROTOCOL_ARWEAVE &&
47
+ raw.recipientProtocol !== ESCROW_PROTOCOL_ETHEREUM) {
48
+ throw new Error(`EscrowAnt: unknown protocol byte ${raw.recipientProtocol}`);
49
+ }
50
+ return {
51
+ version: raw.version,
52
+ bump: raw.bump,
53
+ depositor: raw.depositor,
54
+ antMint: raw.antMint,
55
+ recipientProtocol,
56
+ recipientPubkey: new Uint8Array(raw.recipientPubkey.subarray(0, expectedLen)),
57
+ nonce: new Uint8Array(raw.nonce),
58
+ depositSlot: raw.depositSlot,
59
+ };
60
+ }
61
+ function protocolToByte(p) {
62
+ return p === 'arweave' ? ESCROW_PROTOCOL_ARWEAVE : ESCROW_PROTOCOL_ETHEREUM;
63
+ }
64
+ /**
65
+ * Solana-backed client for the trustless ANT-escrow program. All write
66
+ * methods require both `rpcSubscriptions` and `signer`; read methods
67
+ * only need `rpc`.
68
+ */
69
+ export class ANTEscrow {
70
+ rpc;
71
+ rpcSubscriptions;
72
+ signer;
73
+ programId;
74
+ commitment;
75
+ logger;
76
+ constructor(config) {
77
+ this.rpc = config.rpc;
78
+ this.rpcSubscriptions = config.rpcSubscriptions;
79
+ this.signer = config.signer;
80
+ this.programId = config.programId ?? ARIO_ANT_ESCROW_PROGRAM_ID;
81
+ this.commitment = config.commitment ?? 'confirmed';
82
+ this.logger = config.logger ?? Logger.default;
83
+ }
84
+ static init(config) {
85
+ return new ANTEscrow(config);
86
+ }
87
+ // -------------------------------------------------------------------
88
+ // Reads
89
+ // -------------------------------------------------------------------
90
+ /** Fetch the on-chain `EscrowAnt` for an ANT mint, or `null` if no
91
+ * active escrow exists. Uses the Codama-generated decoder. */
92
+ async get(antMint) {
93
+ const [pda] = await getEscrowAntPDA(antMint, this.programId);
94
+ const account = await fetchMaybeEscrowAnt(this.rpc, pda, {
95
+ commitment: this.commitment,
96
+ });
97
+ if (!account.exists)
98
+ return null;
99
+ return toEscrowAntState(account.data);
100
+ }
101
+ /** Address of the EscrowAnt PDA for an ANT mint (no RPC call). */
102
+ async getPda(antMint) {
103
+ const [pda] = await getEscrowAntPDA(antMint, this.programId);
104
+ return pda;
105
+ }
106
+ // -------------------------------------------------------------------
107
+ // Write — depositor-side
108
+ // -------------------------------------------------------------------
109
+ /**
110
+ * Lock an ANT into escrow. The signer (depositor) must currently own
111
+ * the asset; mpl-core's TransferV1 CPI enforces this.
112
+ *
113
+ * `recipient.publicKey` length must match `recipient.protocol`:
114
+ * - `'arweave'` → 512-byte RSA-4096 modulus (the JWK `n` field)
115
+ * - `'ethereum'` → 20-byte address
116
+ */
117
+ async deposit(args) {
118
+ const signer = this.requireSigner('deposit');
119
+ const ix = await this.depositIx(args, signer);
120
+ return this.send([ix]);
121
+ }
122
+ async depositIx(args, depositor) {
123
+ this.assertPubkeyLen(args.recipient);
124
+ const [escrow] = await getEscrowAntPDA(args.antMint, this.programId);
125
+ return getDepositAntInstruction({
126
+ escrow,
127
+ antAsset: args.antMint,
128
+ depositor,
129
+ recipientProtocol: protocolToByte(args.recipient.protocol),
130
+ recipientPubkey: args.recipient.publicKey,
131
+ }, { programAddress: this.programId });
132
+ }
133
+ /**
134
+ * Re-target the escrow at a new recipient identity. Rotates the
135
+ * on-chain nonce, invalidating any in-flight claim signatures bound
136
+ * to the prior recipient.
137
+ */
138
+ async updateRecipient(args) {
139
+ const signer = this.requireSigner('updateRecipient');
140
+ this.assertPubkeyLen(args.newRecipient);
141
+ const [escrow] = await getEscrowAntPDA(args.antMint, this.programId);
142
+ const ix = getUpdateRecipientInstruction({
143
+ escrow,
144
+ depositor: signer,
145
+ newProtocol: protocolToByte(args.newRecipient.protocol),
146
+ newPubkey: args.newRecipient.publicKey,
147
+ }, { programAddress: this.programId });
148
+ return this.send([ix]);
149
+ }
150
+ /**
151
+ * Pull an escrowed ANT back to the depositor. Closes the escrow PDA
152
+ * and refunds rent.
153
+ */
154
+ async cancel(args) {
155
+ const signer = this.requireSigner('cancel');
156
+ const [escrow] = await getEscrowAntPDA(args.antMint, this.programId);
157
+ const ix = getCancelDepositInstruction({
158
+ escrow,
159
+ antAsset: args.antMint,
160
+ depositor: signer,
161
+ }, { programAddress: this.programId });
162
+ return this.send([ix]);
163
+ }
164
+ // -------------------------------------------------------------------
165
+ // Write — claim
166
+ // -------------------------------------------------------------------
167
+ /**
168
+ * Submit an Arweave RSA-PSS-4096 signature to release the ANT.
169
+ * Anyone can submit (the fee payer = `signer`); only `claimant`
170
+ * receives the ANT, and only the original `depositor` receives rent.
171
+ */
172
+ async claimArweave(args) {
173
+ const escrow = await this.requireEscrow(args.antMint);
174
+ if (escrow.recipientProtocol !== 'arweave') {
175
+ throw new Error(`escrow recipient is ${escrow.recipientProtocol}, not arweave`);
176
+ }
177
+ const ix = await this.claimArweaveIx({
178
+ ...args,
179
+ saltLen: args.saltLen ?? 32,
180
+ depositor: escrow.depositor,
181
+ messageNonce: escrow.nonce,
182
+ });
183
+ // RSA-PSS-4096 verification via sol_big_mod_exp is CU-intensive
184
+ // (~200K+ for the full claim including Transfer CPI). Use 400K to
185
+ // provide comfortable headroom.
186
+ return this.send([ix], 400_000);
187
+ }
188
+ /**
189
+ * Build the ANT-claim-via-Arweave-attested instruction.
190
+ *
191
+ * **API note**: this method previously took user-side RSA-PSS params
192
+ * (`signature`, `saltLen`, `messageNonce`). The on-chain ix was
193
+ * renamed to `claim_ant_arweave_attested` (canonical contracts
194
+ * `ar-io-solana-contracts` PR-19+): verification is now via
195
+ * instruction-introspection of a preceding Ed25519 sigverify ix
196
+ * issued by the off-chain attestor (see
197
+ * `migration/attestor/`). Those data args are no longer fed to the
198
+ * builder. Callers MUST prepend the attestor's sigverify ix to the
199
+ * transaction or it will fail on-chain. A higher-level helper that
200
+ * fetches the attestor's signature and assembles the full tx is
201
+ * tracked as a follow-up.
202
+ *
203
+ * @deprecated Args `signature`, `saltLen`, `messageNonce` are
204
+ * ignored. Use the new attested flow.
205
+ */
206
+ async claimArweaveIx(args) {
207
+ if (args.messageNonce.length !== 32) {
208
+ throw new Error('messageNonce must be 32 bytes');
209
+ }
210
+ const signer = this.requireSigner('claimArweave');
211
+ const [escrow] = await getEscrowAntPDA(args.antMint, this.programId);
212
+ return getClaimAntArweaveAttestedInstruction({
213
+ escrow,
214
+ antAsset: args.antMint,
215
+ claimant: args.claimant,
216
+ depositor: args.depositor,
217
+ payer: signer,
218
+ messageNonce: args.messageNonce,
219
+ }, { programAddress: this.programId });
220
+ }
221
+ /** Submit an Ethereum ECDSA secp256k1 + EIP-191 signature. */
222
+ async claimEthereum(args) {
223
+ const escrow = await this.requireEscrow(args.antMint);
224
+ if (escrow.recipientProtocol !== 'ethereum') {
225
+ throw new Error(`escrow recipient is ${escrow.recipientProtocol}, not ethereum`);
226
+ }
227
+ const ix = await this.claimEthereumIx({
228
+ ...args,
229
+ depositor: escrow.depositor,
230
+ messageNonce: escrow.nonce,
231
+ });
232
+ return this.send([ix]);
233
+ }
234
+ async claimEthereumIx(args) {
235
+ if (args.signature.length !== 65) {
236
+ throw new Error('ethereum signature must be 65 bytes (r||s||v)');
237
+ }
238
+ if (args.messageNonce.length !== 32) {
239
+ throw new Error('messageNonce must be 32 bytes');
240
+ }
241
+ const signer = this.requireSigner('claimEthereum');
242
+ const [escrow] = await getEscrowAntPDA(args.antMint, this.programId);
243
+ return getClaimAntEthereumInstruction({
244
+ escrow,
245
+ antAsset: args.antMint,
246
+ claimant: args.claimant,
247
+ depositor: args.depositor,
248
+ payer: signer,
249
+ messageNonce: args.messageNonce,
250
+ signature: args.signature,
251
+ }, { programAddress: this.programId });
252
+ }
253
+ // -------------------------------------------------------------------
254
+ // Internals
255
+ // -------------------------------------------------------------------
256
+ async send(instructions, computeUnitLimit = 200_000) {
257
+ const signer = this.requireSigner('send');
258
+ if (!this.rpcSubscriptions) {
259
+ throw new Error('ANTEscrow: rpcSubscriptions required for write operations');
260
+ }
261
+ return sendAndConfirm({
262
+ rpc: this.rpc,
263
+ rpcSubscriptions: this.rpcSubscriptions,
264
+ signer,
265
+ instructions,
266
+ commitment: this.commitment,
267
+ computeUnitLimit,
268
+ });
269
+ }
270
+ requireSigner(op) {
271
+ if (!this.signer) {
272
+ throw new Error(`ANTEscrow.${op}: signer is required for writes`);
273
+ }
274
+ return this.signer;
275
+ }
276
+ async requireEscrow(antMint) {
277
+ const escrow = await this.get(antMint);
278
+ if (!escrow) {
279
+ throw new Error(`no escrow found for ANT ${antMint}`);
280
+ }
281
+ return escrow;
282
+ }
283
+ assertPubkeyLen(recipient) {
284
+ const expected = recipient.protocol === 'arweave'
285
+ ? ESCROW_ARWEAVE_PUBKEY_LEN
286
+ : ESCROW_ETHEREUM_PUBKEY_LEN;
287
+ if (recipient.publicKey.length !== expected) {
288
+ throw new Error(`recipient.publicKey: expected ${expected} bytes for protocol=${recipient.protocol}, got ${recipient.publicKey.length}`);
289
+ }
290
+ }
291
+ }
292
+ /**
293
+ * Forward clock-skew buffer (seconds) added to `vault_end_timestamp` before
294
+ * the SDK considers a vault claimable. The SDK reads wall-clock time
295
+ * (`Date.now()`) while the on-chain gate reads Solana cluster time, and
296
+ * the two can disagree by several seconds. The buffer biases every skew
297
+ * race into the *friendly* direction: the SDK rejects when the chain
298
+ * would actually accept (user retries 30s later, succeeds), never the
299
+ * reverse (user submits a doomed tx and sees the raw on-chain error).
300
+ *
301
+ * 30s is conservative — Solana cluster clock typically drifts <2s vs
302
+ * wall clock — but matches the order of magnitude of the previously-used
303
+ * `60s` introspection tolerance in the removed `vault_introspect` module.
304
+ */
305
+ export const CLOCK_SKEW_TOLERANCE_SECONDS = 30n;
306
+ /**
307
+ * Returns `true` when a vault escrow is past its unlock timestamp by at
308
+ * least {@link CLOCK_SKEW_TOLERANCE_SECONDS}. Non-throwing companion to
309
+ * {@link assertVaultClaimable} for UI gating (e.g. enabling/disabling a
310
+ * Submit button without showing an error).
311
+ */
312
+ export function isVaultClaimable(escrow) {
313
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
314
+ return nowSeconds >= escrow.vaultEndTimestamp + CLOCK_SKEW_TOLERANCE_SECONDS;
315
+ }
316
+ /**
317
+ * Pre-flight the on-chain `VaultStillLocked` gate (ADR-022): refuse to build
318
+ * a claim tx while the vault is still locked, with a small forward
319
+ * {@link CLOCK_SKEW_TOLERANCE_SECONDS} buffer so wall/cluster clock skew
320
+ * biases into the friendly direction. Surfaces the unlock timestamp so
321
+ * callers / UIs can show "claimable after <date>" instead of a doomed tx.
322
+ *
323
+ * Exported for unit-testability; not part of the public SDK surface — call the
324
+ * high-level `claimVaultArweave` / `claimVaultEthereum` instead, which invoke
325
+ * this guard internally.
326
+ *
327
+ * @internal
328
+ */
329
+ export function assertVaultClaimable(escrow) {
330
+ if (!isVaultClaimable(escrow)) {
331
+ const unlockIso = new Date(Number(escrow.vaultEndTimestamp) * 1000).toISOString();
332
+ throw new Error(`Vault escrow is still locked until ${unlockIso} ` +
333
+ `(vault_end_timestamp=${escrow.vaultEndTimestamp}; ` +
334
+ `the SDK adds a ${CLOCK_SKEW_TOLERANCE_SECONDS}s clock-skew buffer ` +
335
+ `before allowing a claim). Active (still-locked) vault claims are ` +
336
+ `rejected on-chain with VaultStillLocked (ADR-022) — wait until ` +
337
+ `after the unlock timestamp + buffer, then claim again to receive ` +
338
+ `the tokens liquid.`);
339
+ }
340
+ }
341
+ /** Map the Codama-generated `EscrowToken` raw decoded type to our public
342
+ * `EscrowTokenState` with protocol enum + active-prefix pubkey slice. */
343
+ function toEscrowTokenState(raw) {
344
+ const recipientProtocol = raw.recipientProtocol === ESCROW_PROTOCOL_ARWEAVE ? 'arweave' : 'ethereum';
345
+ if (raw.recipientProtocol !== ESCROW_PROTOCOL_ARWEAVE &&
346
+ raw.recipientProtocol !== ESCROW_PROTOCOL_ETHEREUM) {
347
+ throw new Error(`EscrowToken: unknown protocol byte ${raw.recipientProtocol}`);
348
+ }
349
+ const expectedLen = recipientProtocol === 'arweave'
350
+ ? ESCROW_ARWEAVE_PUBKEY_LEN
351
+ : ESCROW_ETHEREUM_PUBKEY_LEN;
352
+ return {
353
+ version: raw.version,
354
+ bump: raw.bump,
355
+ depositor: raw.depositor,
356
+ assetType: raw.assetType === ESCROW_ASSET_TYPE_VAULT ? 'vault' : 'token',
357
+ amount: raw.amount,
358
+ arioMint: raw.arioMint,
359
+ assetId: new Uint8Array(raw.assetId),
360
+ recipientProtocol,
361
+ recipientPubkey: new Uint8Array(raw.recipientPubkey.subarray(0, expectedLen)),
362
+ nonce: new Uint8Array(raw.nonce),
363
+ depositSlot: raw.depositSlot,
364
+ vaultEndTimestamp: raw.vaultEndTimestamp,
365
+ vaultRevocable: raw.vaultRevocable,
366
+ };
367
+ }
368
+ // =========================================
369
+ // ATA helper
370
+ // =========================================
371
+ /** Associated Token Account program address. */
372
+ const ATA_PROGRAM_ADDRESS = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
373
+ /**
374
+ * Build a `CreateAssociatedTokenAccountIdempotent` instruction.
375
+ * Uses instruction index 1 (idempotent variant) of the ATA program.
376
+ */
377
+ function buildCreateAtaIdempotentIx(payer, ata, owner, mint) {
378
+ const SYSTEM_PROGRAM = '11111111111111111111111111111111';
379
+ const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
380
+ return {
381
+ programAddress: ATA_PROGRAM_ADDRESS,
382
+ accounts: [
383
+ { address: payer, role: 3 }, // writable signer
384
+ { address: ata, role: 1 }, // writable
385
+ { address: owner, role: 0 }, // readonly
386
+ { address: mint, role: 0 }, // readonly
387
+ { address: SYSTEM_PROGRAM, role: 0 }, // readonly
388
+ { address: TOKEN_PROGRAM, role: 0 }, // readonly
389
+ ],
390
+ data: new Uint8Array([1]), // CreateIdempotent = instruction discriminator 1
391
+ };
392
+ }
393
+ // =========================================
394
+ // TokenEscrow client
395
+ // =========================================
396
+ /**
397
+ * Solana-backed client for the trustless token/vault escrow program. All
398
+ * write methods require both `rpcSubscriptions` and `signer`; read methods
399
+ * only need `rpc`.
400
+ *
401
+ * Uses the same config shape as {@link ANTEscrow}.
402
+ */
403
+ export class TokenEscrow {
404
+ rpc;
405
+ rpcSubscriptions;
406
+ signer;
407
+ programId;
408
+ coreProgram;
409
+ commitment;
410
+ logger;
411
+ constructor(config) {
412
+ this.rpc = config.rpc;
413
+ this.rpcSubscriptions = config.rpcSubscriptions;
414
+ this.signer = config.signer;
415
+ this.programId = config.programId ?? ARIO_ANT_ESCROW_PROGRAM_ID;
416
+ this.coreProgram = config.coreProgram ?? ARIO_CORE_PROGRAM_ID;
417
+ this.commitment = config.commitment ?? 'confirmed';
418
+ this.logger = config.logger ?? Logger.default;
419
+ }
420
+ static init(config) {
421
+ return new TokenEscrow(config);
422
+ }
423
+ // -------------------------------------------------------------------
424
+ // Reads
425
+ // -------------------------------------------------------------------
426
+ /**
427
+ * Fetch the on-chain `EscrowToken` for a depositor and asset ID, or
428
+ * `null` if no active escrow exists. Uses the Codama-generated decoder.
429
+ */
430
+ async get(depositor, assetId) {
431
+ const [pda] = await getEscrowTokenPDA(depositor, assetId, this.programId);
432
+ const account = await fetchMaybeEscrowToken(this.rpc, pda, {
433
+ commitment: this.commitment,
434
+ });
435
+ if (!account.exists)
436
+ return null;
437
+ return toEscrowTokenState(account.data);
438
+ }
439
+ /**
440
+ * Fetch the on-chain `EscrowToken` for a vault escrow, or `null` if
441
+ * no active escrow exists.
442
+ */
443
+ async getVault(depositor, assetId) {
444
+ const [pda] = await getEscrowVaultPDA(depositor, assetId, this.programId);
445
+ const account = await fetchMaybeEscrowToken(this.rpc, pda, {
446
+ commitment: this.commitment,
447
+ });
448
+ if (!account.exists)
449
+ return null;
450
+ return toEscrowTokenState(account.data);
451
+ }
452
+ /** Address of the EscrowToken PDA (no RPC call). */
453
+ async getTokenPda(depositor, assetId) {
454
+ const [pda] = await getEscrowTokenPDA(depositor, assetId, this.programId);
455
+ return pda;
456
+ }
457
+ /** Address of the EscrowVault PDA (no RPC call). */
458
+ async getVaultPda(depositor, assetId) {
459
+ const [pda] = await getEscrowVaultPDA(depositor, assetId, this.programId);
460
+ return pda;
461
+ }
462
+ // -------------------------------------------------------------------
463
+ // Write — deposit
464
+ // -------------------------------------------------------------------
465
+ /**
466
+ * Deposit liquid ARIO tokens into escrow for a designated Arweave or
467
+ * Ethereum recipient. Prepends a create-ATA-idempotent instruction for
468
+ * the escrow PDA's token account in the same transaction.
469
+ */
470
+ async depositTokens(args) {
471
+ const signer = this.requireSigner('depositTokens');
472
+ this.assertPubkeyLen(args.recipient);
473
+ this.assertAssetIdLen(args.assetId);
474
+ const [escrow] = await getEscrowTokenPDA(signer.address, args.assetId, this.programId);
475
+ // Derive the escrow PDA's ATA for the ARIO mint.
476
+ const escrowAta = await getAssociatedTokenAddressKit(args.arioMint, escrow, true);
477
+ // Prepend create-ATA-idempotent so the escrow token account exists.
478
+ const createAtaIx = buildCreateAtaIdempotentIx(signer.address, escrowAta, escrow, args.arioMint);
479
+ const depositIx = getDepositTokensInstruction({
480
+ escrow,
481
+ depositorTokenAccount: args.depositorTokenAccount,
482
+ escrowTokenAccount: escrowAta,
483
+ arioMint: args.arioMint,
484
+ depositor: signer,
485
+ assetId: args.assetId,
486
+ amount: args.amount,
487
+ recipientProtocol: protocolToByte(args.recipient.protocol),
488
+ recipientPubkey: args.recipient.publicKey,
489
+ }, { programAddress: this.programId });
490
+ return this.send([createAtaIx, depositIx]);
491
+ }
492
+ /**
493
+ * Deposit ARIO tokens into escrow as a vaulted (time-locked) position.
494
+ * Same as `depositTokens` but additionally records the lock duration
495
+ * and revocability flag. Uses the vault PDA seed.
496
+ */
497
+ async depositVault(args) {
498
+ const signer = this.requireSigner('depositVault');
499
+ this.assertPubkeyLen(args.recipient);
500
+ this.assertAssetIdLen(args.assetId);
501
+ const [escrow] = await getEscrowVaultPDA(signer.address, args.assetId, this.programId);
502
+ const escrowAta = await getAssociatedTokenAddressKit(args.arioMint, escrow, true);
503
+ const createAtaIx = buildCreateAtaIdempotentIx(signer.address, escrowAta, escrow, args.arioMint);
504
+ const depositIx = getDepositVaultInstruction({
505
+ escrow,
506
+ depositorTokenAccount: args.depositorTokenAccount,
507
+ escrowTokenAccount: escrowAta,
508
+ arioMint: args.arioMint,
509
+ depositor: signer,
510
+ assetId: args.assetId,
511
+ amount: args.amount,
512
+ lockDurationSeconds: args.lockDurationSeconds,
513
+ revocable: args.revocable,
514
+ recipientProtocol: protocolToByte(args.recipient.protocol),
515
+ recipientPubkey: args.recipient.publicKey,
516
+ }, { programAddress: this.programId });
517
+ return this.send([createAtaIx, depositIx]);
518
+ }
519
+ // -------------------------------------------------------------------
520
+ // Write — claim
521
+ // -------------------------------------------------------------------
522
+ /**
523
+ * Submit an Arweave RSA-PSS-4096 signature to release escrowed tokens.
524
+ * Anyone can submit (fee payer = `signer`); only `claimant` receives
525
+ * the tokens, and `depositor` receives rent.
526
+ */
527
+ async claimTokensArweave(args) {
528
+ const escrow = await this.requireTokenEscrow(args.depositor, args.assetId);
529
+ if (escrow.recipientProtocol !== 'arweave') {
530
+ throw new Error(`escrow recipient is ${escrow.recipientProtocol}, not arweave`);
531
+ }
532
+ const ix = await this.claimTokensArweaveIx({
533
+ ...args,
534
+ saltLen: args.saltLen ?? 32,
535
+ messageNonce: escrow.nonce,
536
+ });
537
+ // The on-chain claim handler delivers liquid tokens to
538
+ // `claimantTokenAccount`; for fresh-wallet claimants the canonical ATA
539
+ // doesn't exist yet (#3012). Idempotent-create when canonical.
540
+ const createAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
541
+ // RSA-PSS-4096 verification is CU-intensive; use 400K.
542
+ return this.send(createAtaIx ? [createAtaIx, ix] : [ix], 400_000);
543
+ }
544
+ /**
545
+ * @deprecated Args `signature`, `saltLen`, `messageNonce` are ignored.
546
+ * Use the new attested flow — see `claimArweaveIx` doc.
547
+ */
548
+ async claimTokensArweaveIx(args) {
549
+ if (args.messageNonce.length !== 32) {
550
+ throw new Error('messageNonce must be 32 bytes');
551
+ }
552
+ const signer = this.requireSigner('claimTokensArweave');
553
+ const [escrow] = await getEscrowTokenPDA(args.depositor, args.assetId, this.programId);
554
+ return getClaimTokensArweaveAttestedInstruction({
555
+ escrow,
556
+ escrowTokenAccount: args.escrowTokenAccount,
557
+ claimantTokenAccount: args.claimantTokenAccount,
558
+ claimant: args.claimant,
559
+ depositor: args.depositor,
560
+ payer: signer,
561
+ messageNonce: args.messageNonce,
562
+ }, { programAddress: this.programId });
563
+ }
564
+ /**
565
+ * Submit an Ethereum ECDSA secp256k1 + EIP-191 signature to release
566
+ * escrowed tokens.
567
+ */
568
+ async claimTokensEthereum(args) {
569
+ const escrow = await this.requireTokenEscrow(args.depositor, args.assetId);
570
+ if (escrow.recipientProtocol !== 'ethereum') {
571
+ throw new Error(`escrow recipient is ${escrow.recipientProtocol}, not ethereum`);
572
+ }
573
+ const ix = await this.claimTokensEthereumIx({
574
+ ...args,
575
+ messageNonce: escrow.nonce,
576
+ });
577
+ // Same fresh-wallet #3012 vector as claimTokensArweave — bundle a
578
+ // canonical-ATA idempotent-create when applicable.
579
+ const createAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
580
+ return this.send(createAtaIx ? [createAtaIx, ix] : [ix]);
581
+ }
582
+ async claimTokensEthereumIx(args) {
583
+ if (args.signature.length !== 65) {
584
+ throw new Error('ethereum signature must be 65 bytes (r||s||v)');
585
+ }
586
+ if (args.messageNonce.length !== 32) {
587
+ throw new Error('messageNonce must be 32 bytes');
588
+ }
589
+ const signer = this.requireSigner('claimTokensEthereum');
590
+ const [escrow] = await getEscrowTokenPDA(args.depositor, args.assetId, this.programId);
591
+ return getClaimTokensEthereumInstruction({
592
+ escrow,
593
+ escrowTokenAccount: args.escrowTokenAccount,
594
+ claimantTokenAccount: args.claimantTokenAccount,
595
+ claimant: args.claimant,
596
+ depositor: args.depositor,
597
+ payer: signer,
598
+ messageNonce: args.messageNonce,
599
+ signature: args.signature,
600
+ }, { programAddress: this.programId });
601
+ }
602
+ /**
603
+ * **Use {@link claimVaultArweaveIx} instead** — this single-send wrapper
604
+ * cannot work end-to-end for the Arweave attested vault-claim path,
605
+ * because the on-chain `claim_vault_arweave_attested` handler requires
606
+ * an Ed25519Program native sigverify ix at idx-1 of the claim ix
607
+ * (introspected via `instructions_sysvar`). That sigverify ix carries
608
+ * the attestor's Ed25519 signature over the canonical claim message
609
+ * and is built by the *integrator* (who calls the off-chain attestor
610
+ * service for the signature, per ADR-017) — the SDK has no attestor
611
+ * URL or client to do this for the caller.
612
+ *
613
+ * Calling this method instead of composing via
614
+ * {@link claimVaultArweaveIx} will hit `MissingAttestation` on-chain.
615
+ * It is kept only for ABI continuity; new code must use
616
+ * {@link claimVaultArweaveIx} and prepend the attestor sigverify ix.
617
+ *
618
+ * @deprecated cannot succeed alone — see {@link claimVaultArweaveIx}.
619
+ */
620
+ async claimVaultArweave(_args) {
621
+ throw new Error('claimVaultArweave cannot complete the Arweave attested vault-claim ' +
622
+ 'in a single SDK call: the on-chain handler requires an ' +
623
+ 'Ed25519Program sigverify ix at idx-1 of the claim ix, which ' +
624
+ 'must carry the attestor service’s signature over the canonical ' +
625
+ 'claim message (ADR-017). The SDK does not know your attestor URL. ' +
626
+ 'Use claimVaultArweaveIx() instead and bundle the sigverify ix ' +
627
+ 'yourself: ' +
628
+ '[createAtaIx?, ed25519SigverifyIx, claimVaultArweaveIx, ...]. ' +
629
+ 'See ar-io-solana-escrow-app/src/pages/ClaimPage.tsx for the ' +
630
+ 'reference composition, and ADR-022 / VaultStillLocked for the ' +
631
+ 'still-locked rejection (use isVaultClaimable() to pre-flight).');
632
+ }
633
+ /**
634
+ * Build a `claim_vault_arweave_attested` instruction (without sending).
635
+ * Mirror of {@link claimTokensArweaveIx} for the vault-claim path:
636
+ * returns the bare claim ix so the caller can prepend the attestor's
637
+ * Ed25519Program sigverify ix and submit the bundle in one tx.
638
+ *
639
+ * The on-chain handler:
640
+ * - Reads the preceding Ed25519Program native sigverify ix from
641
+ * `instructions_sysvar` to confirm the attestor signed the canonical
642
+ * claim message.
643
+ * - Rejects with `VaultStillLocked` if `clock < vault_end_timestamp`
644
+ * (ADR-022). Callers should pre-flight with {@link isVaultClaimable}
645
+ * (non-throwing) or {@link assertVaultClaimable} (throws with the
646
+ * unlock timestamp) before composing the tx.
647
+ * - On the expired path, transfers the escrowed amount liquid to
648
+ * `claimantTokenAccount` and closes the escrow PDA.
649
+ *
650
+ * Composition (frontend pattern):
651
+ * ```ts
652
+ * const escrow = await tokenEscrow.requireVaultEscrow(depositor, assetId);
653
+ * assertVaultClaimable(escrow); // pre-flight
654
+ * const canonical = canonicalMessage({ ... }); // build message
655
+ * const attestation = await attestor.attest({ ... }); // attestor service
656
+ * const ed25519Ix = buildEd25519SigverifyIx(
657
+ * attestation.attestorPubkey, attestation.signature, canonical,
658
+ * );
659
+ * const claimIx = await tokenEscrow.claimVaultArweaveIx({
660
+ * depositor, assetId, claimant, claimantTokenAccount,
661
+ * escrowTokenAccount, messageNonce: escrow.nonce,
662
+ * });
663
+ * // Idempotent-create the claimant ATA if it's the canonical derivation.
664
+ * await sendTx([createAtaIx?, ed25519Ix, claimIx]);
665
+ * ```
666
+ */
667
+ async claimVaultArweaveIx(args) {
668
+ if (args.messageNonce.length !== 32) {
669
+ throw new Error('messageNonce must be 32 bytes');
670
+ }
671
+ const signer = this.requireSigner('claimVaultArweaveIx');
672
+ const [escrowPda] = await getEscrowVaultPDA(args.depositor, args.assetId, this.programId);
673
+ return getClaimVaultArweaveAttestedInstruction({
674
+ escrow: escrowPda,
675
+ escrowTokenAccount: args.escrowTokenAccount,
676
+ claimantTokenAccount: args.claimantTokenAccount,
677
+ claimant: args.claimant,
678
+ depositor: args.depositor,
679
+ payer: signer,
680
+ messageNonce: args.messageNonce,
681
+ }, { programAddress: this.programId });
682
+ }
683
+ /**
684
+ * Submit an Ethereum ECDSA signature to release escrowed vault tokens. See
685
+ * {@link claimVaultArweave} — same lock semantics: vaults are only claimable
686
+ * after `vault_end_timestamp`; active (still-locked) claims throw pre-flight
687
+ * and are rejected on-chain with `VaultStillLocked` (ADR-022 / BD-107).
688
+ */
689
+ async claimVaultEthereum(args) {
690
+ const escrow = await this.requireVaultEscrow(args.depositor, args.assetId);
691
+ if (escrow.recipientProtocol !== 'ethereum') {
692
+ throw new Error(`escrow recipient is ${escrow.recipientProtocol}, not ethereum`);
693
+ }
694
+ assertVaultClaimable(escrow);
695
+ const signer = this.requireSigner('claimVaultEthereum');
696
+ const [escrowPda] = await getEscrowVaultPDA(args.depositor, args.assetId, this.programId);
697
+ const claimIx = getClaimVaultEthereumInstruction({
698
+ escrow: escrowPda,
699
+ escrowTokenAccount: args.escrowTokenAccount,
700
+ claimantTokenAccount: args.claimantTokenAccount,
701
+ claimant: args.claimant,
702
+ depositor: args.depositor,
703
+ payer: signer,
704
+ messageNonce: escrow.nonce,
705
+ signature: args.signature,
706
+ }, { programAddress: this.programId });
707
+ const createClaimantAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
708
+ const ixs = createClaimantAtaIx
709
+ ? [createClaimantAtaIx, claimIx]
710
+ : [claimIx];
711
+ return this.send(ixs);
712
+ }
713
+ /**
714
+ /**
715
+ * Idempotent-create the claimant's canonical ATA when needed.
716
+ *
717
+ * The claim handler delivers liquid tokens directly to
718
+ * `claimantTokenAccount` (post-ADR-022 there's only the liquid path for
719
+ * vaults). If the claimant is a fresh wallet that has never held this
720
+ * mint, the ATA doesn't exist and the tx fails with `AccountNotInitialized`
721
+ * (#3012).
722
+ *
723
+ * Returns `null` when the caller passed a non-canonical
724
+ * `claimantTokenAccount` (manually-created non-ATA token account,
725
+ * presumably already exists — caller's responsibility).
726
+ */
727
+ async _createClaimantAtaIfCanonical(claimant, claimantTokenAccount, mint) {
728
+ const canonical = await getAssociatedTokenAddressKit(mint, claimant);
729
+ if (claimantTokenAccount !== canonical)
730
+ return null;
731
+ const signer = this.requireSigner('createClaimantAtaIfCanonical');
732
+ return buildCreateAtaIdempotentIx(signer.address, canonical, claimant, mint);
733
+ }
734
+ // -------------------------------------------------------------------
735
+ // Write — cancel
736
+ // -------------------------------------------------------------------
737
+ /**
738
+ * Cancel a token or vault escrow deposit and return the tokens to the
739
+ * depositor. Only callable by the original depositor.
740
+ */
741
+ async cancel(args) {
742
+ const signer = this.requireSigner('cancel');
743
+ this.assertAssetIdLen(args.assetId);
744
+ const pdaFn = args.assetType === 'vault' ? getEscrowVaultPDA : getEscrowTokenPDA;
745
+ const [escrow] = await pdaFn(signer.address, args.assetId, this.programId);
746
+ const ix = args.assetType === 'vault'
747
+ ? getCancelVaultDepositInstruction({
748
+ escrow,
749
+ escrowTokenAccount: args.escrowTokenAccount,
750
+ depositorTokenAccount: args.depositorTokenAccount,
751
+ depositor: signer,
752
+ }, { programAddress: this.programId })
753
+ : getCancelTokenDepositInstruction({
754
+ escrow,
755
+ escrowTokenAccount: args.escrowTokenAccount,
756
+ depositorTokenAccount: args.depositorTokenAccount,
757
+ depositor: signer,
758
+ }, { programAddress: this.programId });
759
+ return this.send([ix]);
760
+ }
761
+ // -------------------------------------------------------------------
762
+ // Write — update recipient
763
+ // -------------------------------------------------------------------
764
+ /**
765
+ * Re-target the escrow at a new recipient identity. Rotates the
766
+ * on-chain nonce, invalidating any in-flight claim signatures.
767
+ */
768
+ async updateRecipient(args) {
769
+ const signer = this.requireSigner('updateRecipient');
770
+ this.assertPubkeyLen(args.newRecipient);
771
+ this.assertAssetIdLen(args.assetId);
772
+ const pdaFn = args.assetType === 'vault' ? getEscrowVaultPDA : getEscrowTokenPDA;
773
+ const [escrow] = await pdaFn(signer.address, args.assetId, this.programId);
774
+ const ix = args.assetType === 'vault'
775
+ ? getUpdateVaultRecipientInstruction({
776
+ escrow,
777
+ depositor: signer,
778
+ newProtocol: protocolToByte(args.newRecipient.protocol),
779
+ newPubkey: args.newRecipient.publicKey,
780
+ }, { programAddress: this.programId })
781
+ : getUpdateTokenRecipientInstruction({
782
+ escrow,
783
+ depositor: signer,
784
+ newProtocol: protocolToByte(args.newRecipient.protocol),
785
+ newPubkey: args.newRecipient.publicKey,
786
+ }, { programAddress: this.programId });
787
+ return this.send([ix]);
788
+ }
789
+ // -------------------------------------------------------------------
790
+ // Internals
791
+ // -------------------------------------------------------------------
792
+ async send(instructions, computeUnitLimit = 200_000) {
793
+ const signer = this.requireSigner('send');
794
+ if (!this.rpcSubscriptions) {
795
+ throw new Error('TokenEscrow: rpcSubscriptions required for write operations');
796
+ }
797
+ return sendAndConfirm({
798
+ rpc: this.rpc,
799
+ rpcSubscriptions: this.rpcSubscriptions,
800
+ signer,
801
+ instructions,
802
+ commitment: this.commitment,
803
+ computeUnitLimit,
804
+ });
805
+ }
806
+ requireSigner(op) {
807
+ if (!this.signer) {
808
+ throw new Error(`TokenEscrow.${op}: signer is required for writes`);
809
+ }
810
+ return this.signer;
811
+ }
812
+ async requireTokenEscrow(depositor, assetId) {
813
+ const escrow = await this.get(depositor, assetId);
814
+ if (!escrow) {
815
+ throw new Error(`no token escrow found for depositor=${depositor}`);
816
+ }
817
+ return escrow;
818
+ }
819
+ async requireVaultEscrow(depositor, assetId) {
820
+ const escrow = await this.getVault(depositor, assetId);
821
+ if (!escrow) {
822
+ throw new Error(`no vault escrow found for depositor=${depositor}`);
823
+ }
824
+ return escrow;
825
+ }
826
+ assertPubkeyLen(recipient) {
827
+ const expected = recipient.protocol === 'arweave'
828
+ ? ESCROW_ARWEAVE_PUBKEY_LEN
829
+ : ESCROW_ETHEREUM_PUBKEY_LEN;
830
+ if (recipient.publicKey.length !== expected) {
831
+ throw new Error(`recipient.publicKey: expected ${expected} bytes for protocol=${recipient.protocol}, got ${recipient.publicKey.length}`);
832
+ }
833
+ }
834
+ assertAssetIdLen(assetId) {
835
+ if (assetId.length !== 32) {
836
+ throw new Error(`assetId must be 32 bytes, got ${assetId.length}`);
837
+ }
838
+ }
839
+ }