@ar.io/sdk 3.24.0 → 4.0.0-alpha.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.
Files changed (169) hide show
  1. package/README.md +682 -600
  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 +221 -0
  6. package/lib/esm/cli/commands/gatewayWriteCommands.js +142 -23
  7. package/lib/esm/cli/commands/pruneCommands.js +150 -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 +280 -174
  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 +11 -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 +51 -263
  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,384 @@
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 implementation of ANT (Arweave Name Token) write interface.
18
+ *
19
+ * Extends SolanaANTReadable with write operations that build and send
20
+ * Solana transactions to the ario-ant program.
21
+ *
22
+ * All instruction encoding is delegated to the Codama-generated builders in
23
+ * `./generated/ant/instructions/` — they own the discriminator + Borsh codec
24
+ * + account-meta wiring derived from the on-chain IDL.
25
+ *
26
+ * ACL maintenance (ADR-012, paginated per-user ACL):
27
+ * The on-chain handlers for `add_controller`, `remove_controller`, and
28
+ * `transfer` *require* the controller / new-owner / old-owner ACL
29
+ * accounts as instruction inputs and write the ACL inline as part of
30
+ * the same atomic ix. This SDK's job is therefore reduced to two
31
+ * things:
32
+ * 1. **Preflight resolution** — ask the composed
33
+ * `SolanaANTRegistryWriteable` to pick the right page and emit
34
+ * any `register_acl_config` / `add_acl_page` ixs that need to be
35
+ * prepended so the contract's account validation succeeds.
36
+ * 2. **Ex-controller cleanup on transfer** — a wrapped transfer
37
+ * cannot atomically `swap_remove` every ex-controller's ACL
38
+ * entry (variable cardinality, no clean way to express variadic
39
+ * accounts in Codama). The SDK reads the controllers list and
40
+ * delegates to `registry.bulkRemoveControllerEntries`, which
41
+ * produces the right `remove_acl_controller` ixs to append into
42
+ * the transfer tx. Permissionless heal flows clean up any drift
43
+ * if a marketplace transfer bypasses this SDK entirely.
44
+ */
45
+ import { address, } from '@solana/kit';
46
+ import { getAddControllerInstructionAsync, getMigrateAntInstructionAsync, getReconcileInstructionAsync, getRemoveControllerInstructionAsync, getRemoveRecordInstructionAsync, getSetDescriptionInstructionAsync, getSetKeywordsInstructionAsync, getSetLogoInstructionAsync, getSetNameInstructionAsync, getSetRecordInstructionAsync, getSetRecordMetadataInstructionAsync, getSetTickerInstructionAsync, getTransferInstructionAsync, getTransferRecordInstructionAsync, } from '@ar.io/solana-contracts/ant';
47
+ import { SolanaANTReadable } from './ant-readable.js';
48
+ import { SolanaANTRegistryWriteable } from './ant-registry-writeable.js';
49
+ import { deserializeAntControllers } from './deserialize.js';
50
+ import { getAccountInfoLegacy } from './json-rpc.js';
51
+ import { getAntControllersPDA, getAntRecordMetadataPDA, getAntRecordPDA, } from './pda.js';
52
+ import { sendAndConfirm } from './send.js';
53
+ /**
54
+ * Solana-backed read-write client for a single ANT (Arweave Name Token).
55
+ *
56
+ * Usage:
57
+ * ```ts
58
+ * import { createSolanaRpc, createSolanaRpcSubscriptions, createKeyPairSignerFromBytes } from '@solana/kit';
59
+ * import { SolanaANTWriteable } from '@ar.io/sdk/solana';
60
+ *
61
+ * const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
62
+ * const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');
63
+ * const signer = await createKeyPairSignerFromBytes(secretKeyBytes);
64
+ * const ant = new SolanaANTWriteable({
65
+ * rpc,
66
+ * rpcSubscriptions,
67
+ * processId: 'MetaplexCoreAssetAddress...',
68
+ * signer,
69
+ * });
70
+ *
71
+ * await ant.setRecord({ undername: 'docs', transactionId: '...', ttlSeconds: 3600 });
72
+ * ```
73
+ */
74
+ export class SolanaANTWriteable extends SolanaANTReadable {
75
+ signer;
76
+ rpcSubscriptions;
77
+ constructor(config) {
78
+ const registry = config.registry ??
79
+ new SolanaANTRegistryWriteable({
80
+ rpc: config.rpc,
81
+ signer: config.signer,
82
+ commitment: config.commitment,
83
+ logger: config.logger,
84
+ antProgramId: config.antProgramId,
85
+ });
86
+ super({ ...config, registry });
87
+ this.signer = config.signer;
88
+ this.rpcSubscriptions = config.rpcSubscriptions;
89
+ }
90
+ /**
91
+ * Build, sign, and send a transaction.
92
+ *
93
+ * Plain pass-through to `sendAndConfirm` — every ANT write whose ACL
94
+ * footprint is bounded (controllers add/remove, owner swap on
95
+ * transfer) is handled inline by the contract handlers. Variable-
96
+ * length cleanup (notably ex-controller wipe after transfer) is
97
+ * pre-built by the caller via `registry.bulkRemoveControllerEntries`
98
+ * and appended into the same `instructions` array, so this method
99
+ * doesn't need its own ACL plumbing.
100
+ */
101
+ async sendTransaction(instructions) {
102
+ return sendAndConfirm({
103
+ rpc: this.rpc,
104
+ rpcSubscriptions: this.rpcSubscriptions,
105
+ signer: this.signer,
106
+ instructions,
107
+ commitment: this.commitment,
108
+ computeUnitLimit: 400_000,
109
+ });
110
+ }
111
+ // =========================================
112
+ // Record operations
113
+ // =========================================
114
+ async setRecord(params, _options) {
115
+ const [recordPda] = await getAntRecordPDA(this.mint, params.undername, this.antProgram);
116
+ // Per-record metadata (display name / logo / description / keywords)
117
+ // moved out of `set_record` into a dedicated `set_record_metadata`
118
+ // instruction when `AntRecord` was split (ARNS-712). These fields are
119
+ // not part of `ANTSetUndernameRecordParams` today, but we keep the
120
+ // forward path here so callers can pass them on the params object and
121
+ // we'll bundle a `set_record_metadata` ix into the same tx.
122
+ const extra = params;
123
+ const ixs = [];
124
+ ixs.push(await getSetRecordInstructionAsync({
125
+ asset: this.mint,
126
+ record: recordPda,
127
+ caller: this.signer,
128
+ undername: params.undername,
129
+ target: params.transactionId,
130
+ targetProtocol: params.targetProtocol ?? 0,
131
+ ttlSeconds: params.ttlSeconds,
132
+ priority: params.priority ?? null,
133
+ recordOwner: params.owner ? address(params.owner) : null,
134
+ }, { programAddress: this.antProgram }));
135
+ const hasMetadata = extra.displayName !== undefined ||
136
+ extra.logo !== undefined ||
137
+ extra.description !== undefined ||
138
+ extra.keywords !== undefined;
139
+ if (hasMetadata) {
140
+ const [metadataPda] = await getAntRecordMetadataPDA(this.mint, params.undername, this.antProgram);
141
+ ixs.push(await getSetRecordMetadataInstructionAsync({
142
+ asset: this.mint,
143
+ record: recordPda,
144
+ recordMetadata: metadataPda,
145
+ caller: this.signer,
146
+ undername: params.undername,
147
+ displayName: extra.displayName ?? null,
148
+ recordLogo: extra.logo ?? null,
149
+ recordDescription: extra.description ?? null,
150
+ recordKeywords: extra.keywords ?? null,
151
+ }, { programAddress: this.antProgram }));
152
+ }
153
+ const sig = await this.sendTransaction(ixs);
154
+ return { id: sig };
155
+ }
156
+ async setBaseNameRecord(params, options) {
157
+ return this.setRecord({ ...params, undername: '@' }, options);
158
+ }
159
+ async setUndernameRecord(params, options) {
160
+ return this.setRecord(params, options);
161
+ }
162
+ async removeRecord(params, _options) {
163
+ const [recordPda] = await getAntRecordPDA(this.mint, params.undername, this.antProgram);
164
+ const ix = await getRemoveRecordInstructionAsync({
165
+ asset: this.mint,
166
+ record: recordPda,
167
+ caller: this.signer,
168
+ }, { programAddress: this.antProgram });
169
+ const sig = await this.sendTransaction([ix]);
170
+ return { id: sig };
171
+ }
172
+ async removeUndernameRecord(params, options) {
173
+ return this.removeRecord(params, options);
174
+ }
175
+ async transferRecord(params, _options) {
176
+ const [recordPda] = await getAntRecordPDA(this.mint, params.undername, this.antProgram);
177
+ const ix = await getTransferRecordInstructionAsync({
178
+ asset: this.mint,
179
+ record: recordPda,
180
+ caller: this.signer,
181
+ newOwner: address(params.recipient),
182
+ }, { programAddress: this.antProgram });
183
+ const sig = await this.sendTransaction([ix]);
184
+ return { id: sig };
185
+ }
186
+ // =========================================
187
+ // Controller operations
188
+ // =========================================
189
+ async addController(params, _options) {
190
+ const controller = address(params.controller);
191
+ // ADR-012 (ACL): contract `add_controller` requires the controller's
192
+ // `AclConfig` + a destination `AclPage` and writes the
193
+ // `record_controller` entry inline. Resolve them here and prepend
194
+ // any `register_acl_config` / `add_acl_page` bootstrap ixs needed
195
+ // for first-time controllers.
196
+ const dest = await this.registry.resolveDestinationAclAccounts({
197
+ user: controller,
198
+ });
199
+ const addIx = await getAddControllerInstructionAsync({
200
+ asset: this.mint,
201
+ caller: this.signer,
202
+ controller,
203
+ controllerAclConfig: dest.aclConfigPda,
204
+ controllerAclPage: dest.aclPagePda,
205
+ }, { programAddress: this.antProgram });
206
+ const sig = await this.sendTransaction([...dest.prepIxs, addIx]);
207
+ return { id: sig };
208
+ }
209
+ async removeController(params, _options) {
210
+ const controller = address(params.controller);
211
+ // ADR-012 (ACL): contract `remove_controller` requires the page
212
+ // currently holding `(asset, Controller)`. Resolve via the
213
+ // registry; if the entry can't be found we still pass page 0 as a
214
+ // placeholder so the on-chain handler surfaces `AclEntryNotFound`
215
+ // (or, more likely, the page-belongs check fails first) rather
216
+ // than us silently swallowing the removal.
217
+ const source = await this.registry.resolveSourceAclAccountsForEntry({
218
+ user: controller,
219
+ asset: this.mint,
220
+ role: 'controller',
221
+ });
222
+ const aclConfigPda = source?.aclConfigPda ??
223
+ (await this.registry.deriveAclConfigPda(controller));
224
+ const aclPagePda = source?.aclPagePda ??
225
+ (await this.registry.deriveAclPagePda(controller, 0n));
226
+ const removeIx = await getRemoveControllerInstructionAsync({
227
+ asset: this.mint,
228
+ caller: this.signer,
229
+ controller,
230
+ controllerAclConfig: aclConfigPda,
231
+ controllerAclPage: aclPagePda,
232
+ }, { programAddress: this.antProgram });
233
+ const sig = await this.sendTransaction([removeIx]);
234
+ return { id: sig };
235
+ }
236
+ // =========================================
237
+ // Metadata operations
238
+ // =========================================
239
+ async setName(params, _options) {
240
+ const ix = await getSetNameInstructionAsync({ asset: this.mint, caller: this.signer, name: params.name }, { programAddress: this.antProgram });
241
+ const sig = await this.sendTransaction([ix]);
242
+ return { id: sig };
243
+ }
244
+ async setTicker(params, _options) {
245
+ const ix = await getSetTickerInstructionAsync({ asset: this.mint, caller: this.signer, ticker: params.ticker }, { programAddress: this.antProgram });
246
+ const sig = await this.sendTransaction([ix]);
247
+ return { id: sig };
248
+ }
249
+ async setDescription(params, _options) {
250
+ const ix = await getSetDescriptionInstructionAsync({
251
+ asset: this.mint,
252
+ caller: this.signer,
253
+ description: params.description,
254
+ }, { programAddress: this.antProgram });
255
+ const sig = await this.sendTransaction([ix]);
256
+ return { id: sig };
257
+ }
258
+ async setKeywords(params, _options) {
259
+ const ix = await getSetKeywordsInstructionAsync({ asset: this.mint, caller: this.signer, keywords: params.keywords }, { programAddress: this.antProgram });
260
+ const sig = await this.sendTransaction([ix]);
261
+ return { id: sig };
262
+ }
263
+ async setLogo(params, _options) {
264
+ const ix = await getSetLogoInstructionAsync({ asset: this.mint, caller: this.signer, logo: params.txId }, { programAddress: this.antProgram });
265
+ const sig = await this.sendTransaction([ix]);
266
+ return { id: sig };
267
+ }
268
+ // =========================================
269
+ // Transfer (wrapped ario_ant::transfer — CPI into MPL Core +
270
+ // inline reconcile + inline owner ACL swap)
271
+ // =========================================
272
+ async transfer(params, _options) {
273
+ const newOwner = address(params.target);
274
+ const oldOwner = this.signer.address;
275
+ // Resolve the new owner's destination ACL accounts (and bootstrap
276
+ // them if missing). The contract requires a non-full page; the
277
+ // registry preflight emits `register_acl_config` / `add_acl_page`
278
+ // ixs so the wrapped `transfer` ix's account validation succeeds.
279
+ const newOwnerDest = await this.registry.resolveDestinationAclAccounts({
280
+ user: newOwner,
281
+ });
282
+ // Resolve the old owner's source ACL accounts. If the entry is
283
+ // missing (e.g. the ANT was acquired via a marketplace transfer or
284
+ // before the ACL system existed), heal it by bootstrapping the
285
+ // config/page and recording the owner entry so the on-chain
286
+ // transfer handler can successfully remove it.
287
+ const oldOwnerSource = await this.registry.resolveSourceAclAccountsForEntry({
288
+ user: oldOwner,
289
+ asset: this.mint,
290
+ role: 'owner',
291
+ });
292
+ let oldOwnerAclConfigPda;
293
+ let oldOwnerAclPagePda;
294
+ const oldOwnerHealIxs = [];
295
+ if (oldOwnerSource) {
296
+ oldOwnerAclConfigPda = oldOwnerSource.aclConfigPda;
297
+ oldOwnerAclPagePda = oldOwnerSource.aclPagePda;
298
+ }
299
+ else {
300
+ const dest = await this.registry.resolveDestinationAclAccounts({
301
+ user: oldOwner,
302
+ });
303
+ oldOwnerAclConfigPda = dest.aclConfigPda;
304
+ oldOwnerAclPagePda = dest.aclPagePda;
305
+ oldOwnerHealIxs.push(...dest.prepIxs);
306
+ oldOwnerHealIxs.push(await this.registry.buildRecordIx({
307
+ user: oldOwner,
308
+ asset: this.mint,
309
+ role: 'owner',
310
+ pageIdx: dest.pageIdx,
311
+ }));
312
+ }
313
+ const transferIx = await getTransferInstructionAsync({
314
+ asset: this.mint,
315
+ caller: this.signer,
316
+ newOwner,
317
+ newOwnerAclConfig: newOwnerDest.aclConfigPda,
318
+ newOwnerAclPage: newOwnerDest.aclPagePda,
319
+ oldOwnerAclConfig: oldOwnerAclConfigPda,
320
+ oldOwnerAclPage: oldOwnerAclPagePda,
321
+ }, { programAddress: this.antProgram });
322
+ // Ex-controller cleanup. The wrapped contract handler clears
323
+ // `AntControllers` via inline reconcile, but it cannot atomically
324
+ // `swap_remove` each ex-controller's ACL entry (variable-length;
325
+ // accounts can't be cleanly variadic in Codama). Read the live
326
+ // controllers list before the transfer and let the registry build
327
+ // the cleanup ixs — same-tx so frontends never see a stale "I
328
+ // control this ANT" entry. Permissionless heal flows clean up any
329
+ // drift if a marketplace transfer bypasses this SDK entirely.
330
+ const [controllersPda] = await getAntControllersPDA(this.mint, this.antProgram);
331
+ const controllersAccount = await getAccountInfoLegacy(this.rpc, controllersPda, this.commitment);
332
+ const exControllers = controllersAccount
333
+ ? deserializeAntControllers(controllersAccount.data).controllers
334
+ : [];
335
+ const cleanupIxs = await this.registry.bulkRemoveControllerEntries({
336
+ asset: this.mint,
337
+ controllers: exControllers,
338
+ });
339
+ const sig = await this.sendTransaction([
340
+ ...oldOwnerHealIxs,
341
+ ...newOwnerDest.prepIxs,
342
+ transferIx,
343
+ ...cleanupIxs,
344
+ ]);
345
+ return { id: sig };
346
+ }
347
+ // =========================================
348
+ // Reconcile (Solana-specific)
349
+ // =========================================
350
+ async reconcile(_options) {
351
+ const ix = await getReconcileInstructionAsync({ asset: this.mint, caller: this.signer }, { programAddress: this.antProgram });
352
+ const sig = await this.sendTransaction([ix]);
353
+ return { id: sig };
354
+ }
355
+ // =========================================
356
+ // AO-specific methods (not applicable on Solana)
357
+ // =========================================
358
+ async releaseName(_params, _options) {
359
+ throw new Error('releaseName not applicable on Solana — use ario-arns program directly');
360
+ }
361
+ async reassignName(_params, _options) {
362
+ throw new Error('reassignName not applicable on Solana — use ario-arns program directly');
363
+ }
364
+ async approvePrimaryNameRequest(_params, _options) {
365
+ throw new Error('approvePrimaryNameRequest not applicable on Solana — use ario-core program directly');
366
+ }
367
+ async removePrimaryNames(_params, _options) {
368
+ throw new Error('removePrimaryNames not applicable on Solana — use ario-core program directly');
369
+ }
370
+ /**
371
+ * Migrate this ANT's on-chain state to the latest schema version.
372
+ * On Solana, "upgrade" means per-ANT data migration, not process forking.
373
+ * Returns the transaction signature if migration was needed.
374
+ */
375
+ async upgrade(_params) {
376
+ const needs = await this.needsMigration();
377
+ if (!needs) {
378
+ return { id: '', needsMigration: false };
379
+ }
380
+ const ix = await getMigrateAntInstructionAsync({ asset: this.mint, payer: this.signer }, { programAddress: this.antProgram });
381
+ const sig = await this.sendTransaction([ix]);
382
+ return { id: sig, needsMigration: true };
383
+ }
384
+ }
@@ -0,0 +1,70 @@
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
+ * Associated Token Account derivation, kit-native.
18
+ * Mirrors the classic SPL `getAssociatedTokenAddress` function but returns
19
+ * a kit `Address` without the web3.js dependency.
20
+ *
21
+ * TODO(C7): replace with `findAssociatedTokenPda` from `@solana-program/associated-token`
22
+ * once that package is added as a dependency.
23
+ */
24
+ import { address, getAddressEncoder, getProgramDerivedAddress, } from '@solana/kit';
25
+ import { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, } from './instruction.js';
26
+ export const ATA_PROGRAM_ADDRESS = address('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
27
+ const addressEncoder = getAddressEncoder();
28
+ /**
29
+ * Derive the Associated Token Account (ATA) address for a given owner + mint.
30
+ *
31
+ * @param mint — the SPL token mint address
32
+ * @param owner — the owner's wallet address (or a PDA if `allowOwnerOffCurve = true`)
33
+ * @param allowOwnerOffCurve — unused; kit derives regardless of curve. Kept for
34
+ * parity with the old `@solana/spl-token` signature so call sites don't change.
35
+ */
36
+ export async function getAssociatedTokenAddressKit(mint, owner, _allowOwnerOffCurve = false) {
37
+ const [ata] = await getProgramDerivedAddress({
38
+ programAddress: ATA_PROGRAM_ADDRESS,
39
+ seeds: [
40
+ addressEncoder.encode(owner),
41
+ addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
42
+ addressEncoder.encode(mint),
43
+ ],
44
+ });
45
+ return ata;
46
+ }
47
+ /**
48
+ * Build an idempotent CreateAssociatedTokenAccount instruction. Safe to
49
+ * include in any tx — if the ATA already exists, the SPL ATA program
50
+ * silently succeeds.
51
+ *
52
+ * Used to pre-create vault / escrow ATAs in the same tx as the program
53
+ * instruction that consumes them. Anchor's `Account<TokenAccount>` constraint
54
+ * does NOT init the account, so the caller is responsible.
55
+ */
56
+ export function buildCreateAtaIdempotentIx(payer, ata, owner, mint) {
57
+ return {
58
+ programAddress: ATA_PROGRAM_ADDRESS,
59
+ accounts: [
60
+ // role 3 = writable signer; role 1 = writable; role 0 = readonly
61
+ { address: payer, role: 3 },
62
+ { address: ata, role: 1 },
63
+ { address: owner, role: 0 },
64
+ { address: mint, role: 0 },
65
+ { address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
66
+ { address: TOKEN_PROGRAM_ADDRESS, role: 0 },
67
+ ],
68
+ data: new Uint8Array([1]), // SPL ATA program: 1 = CreateIdempotent
69
+ };
70
+ }
@@ -0,0 +1,128 @@
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
+ * Canonical claim-message helper for `ario-ant-escrow`.
18
+ *
19
+ * Produces the EXACT bytes a recipient signs to release an escrowed
20
+ * ANT. Output MUST be byte-identical to the Rust implementation in
21
+ * `ario-ant-escrow/src/canonical.rs::build_ant_escrow_claim_message`
22
+ * (header `ANT_ESCROW_CLAIM_HEADER = "ar.io ant-escrow claim"`). The on-chain
23
+ * program reconstructs these exact bytes and verifies the signature against
24
+ * them, so any drift (header, field set, or ordering) makes every claim fail
25
+ * `EthereumAddressMismatch` / signature verification.
26
+ *
27
+ * Format (UTF-8, line-feed separated, no trailing newline):
28
+ *
29
+ * ```text
30
+ * ar.io ant-escrow claim
31
+ * network: <network>
32
+ * recipient: <base64url(sha256(recipient_pubkey)) — 43 chars, no pad>
33
+ * ant: <ant_mint_base58>
34
+ * claimant: <claimant_solana_pubkey_base58>
35
+ * nonce: <nonce_hex_lowercase>
36
+ * ```
37
+ *
38
+ * Wallets sign these bytes directly:
39
+ * - Arweave: `wallet.signMessage(bytes)` → 512-byte RSA-PSS sig
40
+ * - Ethereum: `wallet.signMessage(bytes)` → 65-byte ECDSA + EIP-191 sig
41
+ * (the wallet applies the EIP-191 prefix; on-chain code re-applies it).
42
+ */
43
+ import { sha256 } from '@noble/hashes/sha256';
44
+ /** Header literal — must match Rust `ANT_ESCROW_CLAIM_HEADER`. */
45
+ const CANONICAL_HEADER = 'ar.io ant-escrow claim';
46
+ /**
47
+ * Build the canonical claim message bytes. UTF-8 encoded, no trailing
48
+ * newline, exactly the format shown in the docstring.
49
+ *
50
+ * @throws if `nonce` isn't exactly 32 bytes — guards against accidentally
51
+ * passing a hex string or a different-sized buffer.
52
+ */
53
+ export function canonicalMessage(input) {
54
+ if (input.nonce.length !== 32) {
55
+ throw new Error(`canonicalMessage: nonce must be 32 bytes, got ${input.nonce.length}`);
56
+ }
57
+ const text = `${CANONICAL_HEADER}\n` +
58
+ `network: ${input.network}\n` +
59
+ `recipient: ${deriveRecipientId(input.recipient)}\n` +
60
+ `ant: ${input.antMint}\n` +
61
+ `claimant: ${input.claimant}\n` +
62
+ `nonce: ${bytesToHexLower(input.nonce)}`;
63
+ return new TextEncoder().encode(text);
64
+ }
65
+ /** Header literal — must match Rust `ESCROW_CLAIM_HEADER`. */
66
+ const CANONICAL_HEADER_V2 = 'ar.io escrow claim';
67
+ /**
68
+ * Build the v2 canonical claim message bytes for token/vault escrows.
69
+ * UTF-8 encoded, no trailing newline.
70
+ *
71
+ * Format:
72
+ * ```text
73
+ * ar.io escrow claim
74
+ * network: <network>
75
+ * recipient: <base64url(sha256(recipient_pubkey)) — 43 chars, no pad>
76
+ * type: <token|vault>
77
+ * asset: <asset_id_hex_lowercase_64chars>
78
+ * amount: <u64_decimal>
79
+ * claimant: <base58>
80
+ * nonce: <hex_lowercase_64chars>
81
+ * ```
82
+ *
83
+ * @throws if `assetId` or `nonce` aren't exactly 32 bytes.
84
+ */
85
+ export function canonicalMessageV2(input) {
86
+ if (input.assetId.length !== 32) {
87
+ throw new Error(`canonicalMessageV2: assetId must be 32 bytes, got ${input.assetId.length}`);
88
+ }
89
+ if (input.nonce.length !== 32) {
90
+ throw new Error(`canonicalMessageV2: nonce must be 32 bytes, got ${input.nonce.length}`);
91
+ }
92
+ const text = `${CANONICAL_HEADER_V2}\n` +
93
+ `network: ${input.network}\n` +
94
+ `recipient: ${deriveRecipientId(input.recipient)}\n` +
95
+ `type: ${input.assetType}\n` +
96
+ `asset: ${bytesToHexLower(input.assetId)}\n` +
97
+ `amount: ${input.amount.toString()}\n` +
98
+ `claimant: ${input.claimant}\n` +
99
+ `nonce: ${bytesToHexLower(input.nonce)}`;
100
+ return new TextEncoder().encode(text);
101
+ }
102
+ // =========================================
103
+ // Shared utilities
104
+ // =========================================
105
+ /**
106
+ * Recipient identity bound into the claim message — `base64url(sha256(bytes))`
107
+ * with no padding (32-byte hash → 43 chars). Byte-identical to the contract's
108
+ * `canonical.rs::derive_recipient_id_b64url`. Input is the recipient pubkey
109
+ * bytes the deposit targeted (ETH 20-byte address / Solana 32-byte pubkey /
110
+ * Arweave RSA modulus, etc.).
111
+ */
112
+ export function deriveRecipientId(recipient) {
113
+ if (recipient.length === 0) {
114
+ throw new Error('deriveRecipientId: recipient bytes must be non-empty');
115
+ }
116
+ // Node's 'base64url' encoding is unpadded — matches the Rust no-pad alphabet.
117
+ return Buffer.from(sha256(recipient)).toString('base64url');
118
+ }
119
+ /** Lowercase-hex encoding. Matches Rust `encode_hex_lowercase`. */
120
+ export function bytesToHexLower(bytes) {
121
+ let s = '';
122
+ for (let i = 0; i < bytes.length; i++) {
123
+ const b = bytes[i];
124
+ s += (b >>> 4).toString(16);
125
+ s += (b & 0x0f).toString(16);
126
+ }
127
+ return s;
128
+ }