@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.
- package/README.md +757 -589
- package/lib/esm/cli/cli.js +188 -152
- package/lib/esm/cli/commands/antCommands.js +23 -58
- package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
- package/lib/esm/cli/commands/escrowCommands.js +227 -0
- package/lib/esm/cli/commands/gatewayWriteCommands.js +140 -23
- package/lib/esm/cli/commands/pruneCommands.js +154 -0
- package/lib/esm/cli/commands/readCommands.js +22 -3
- package/lib/esm/cli/commands/transfer.js +6 -6
- package/lib/esm/cli/options.js +124 -58
- package/lib/esm/cli/utils.js +303 -175
- package/lib/esm/common/ant-registry.js +17 -143
- package/lib/esm/common/ant.js +44 -1167
- package/lib/esm/common/faucet.js +17 -6
- package/lib/esm/common/index.js +0 -4
- package/lib/esm/common/io.js +25 -1412
- package/lib/esm/constants.js +13 -19
- package/lib/esm/solana/ant-readable.js +724 -0
- package/lib/esm/solana/ant-registry-readable.js +133 -0
- package/lib/esm/solana/ant-registry-writeable.js +472 -0
- package/lib/esm/solana/ant-writeable.js +384 -0
- package/lib/esm/solana/ata.js +70 -0
- package/lib/esm/solana/canonical-message.js +128 -0
- package/lib/esm/solana/clusters.js +111 -0
- package/lib/esm/solana/constants.js +146 -0
- package/lib/esm/solana/delegation-math.js +112 -0
- package/lib/esm/solana/deserialize.js +711 -0
- package/lib/esm/solana/escrow.js +839 -0
- package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
- package/lib/esm/solana/funding-plan.js +699 -0
- package/lib/esm/solana/index.js +126 -0
- package/lib/esm/solana/instruction.js +39 -0
- package/lib/esm/solana/io-readable.js +2182 -0
- package/lib/esm/solana/io-writeable.js +3196 -0
- package/lib/esm/solana/json-rpc.js +90 -0
- package/lib/esm/solana/metadata.js +81 -0
- package/lib/esm/solana/mpl-core.js +192 -0
- package/lib/esm/solana/pda.js +332 -0
- package/lib/esm/solana/predict-prescribed-observers.js +110 -0
- package/lib/esm/solana/retry.js +117 -0
- package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
- package/lib/esm/solana/send.js +372 -0
- package/lib/esm/solana/spawn-ant.js +224 -0
- package/lib/esm/solana/types.js +1 -0
- package/lib/esm/types/ant.js +27 -15
- package/lib/esm/types/io.js +8 -11
- package/lib/esm/utils/ant.js +0 -63
- package/lib/esm/utils/index.js +0 -3
- package/lib/esm/version.js +1 -1
- package/lib/types/cli/commands/antCommands.d.ts +5 -13
- package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
- package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
- package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
- package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
- package/lib/types/cli/commands/readCommands.d.ts +27 -22
- package/lib/types/cli/commands/transfer.d.ts +9 -9
- package/lib/types/cli/options.d.ts +76 -21
- package/lib/types/cli/types.d.ts +11 -13
- package/lib/types/cli/utils.d.ts +71 -31
- package/lib/types/common/ant-registry.d.ts +49 -47
- package/lib/types/common/ant.d.ts +54 -539
- package/lib/types/common/faucet.d.ts +20 -8
- package/lib/types/common/index.d.ts +0 -3
- package/lib/types/common/io.d.ts +66 -258
- package/lib/types/constants.d.ts +11 -18
- package/lib/types/solana/ant-readable.d.ts +180 -0
- package/lib/types/solana/ant-registry-readable.d.ts +105 -0
- package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
- package/lib/types/solana/ant-writeable.d.ts +177 -0
- package/lib/types/solana/ata.d.ts +44 -0
- package/lib/types/solana/canonical-message.d.ts +121 -0
- package/lib/types/solana/clusters.d.ts +109 -0
- package/lib/types/solana/constants.d.ts +119 -0
- package/lib/types/solana/delegation-math.d.ts +45 -0
- package/lib/types/solana/deserialize.d.ts +262 -0
- package/lib/types/solana/escrow.d.ts +480 -0
- package/lib/types/solana/events.d.ts +38 -0
- package/lib/types/solana/funding-plan.d.ts +225 -0
- package/lib/types/solana/index.d.ts +87 -0
- package/lib/types/solana/instruction.d.ts +39 -0
- package/lib/types/solana/io-readable.d.ts +499 -0
- package/lib/types/solana/io-writeable.d.ts +893 -0
- package/lib/types/solana/json-rpc.d.ts +47 -0
- package/lib/types/solana/metadata.d.ts +84 -0
- package/lib/types/solana/mpl-core.d.ts +120 -0
- package/lib/types/solana/pda.d.ts +95 -0
- package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
- package/lib/types/solana/retry.d.ts +62 -0
- package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
- package/lib/types/solana/send.d.ts +94 -0
- package/lib/types/solana/spawn-ant.d.ts +145 -0
- package/lib/types/solana/types.d.ts +82 -0
- package/lib/types/types/ant-registry.d.ts +43 -4
- package/lib/types/types/ant.d.ts +114 -96
- package/lib/types/types/common.d.ts +18 -74
- package/lib/types/types/faucet.d.ts +2 -2
- package/lib/types/types/io.d.ts +244 -158
- package/lib/types/types/token.d.ts +0 -12
- package/lib/types/utils/ant.d.ts +1 -12
- package/lib/types/utils/index.d.ts +0 -3
- package/lib/types/version.d.ts +1 -1
- package/package.json +36 -33
- package/lib/cjs/cli/cli.js +0 -822
- package/lib/cjs/cli/commands/antCommands.js +0 -113
- package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
- package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
- package/lib/cjs/cli/commands/readCommands.js +0 -215
- package/lib/cjs/cli/commands/transfer.js +0 -159
- package/lib/cjs/cli/options.js +0 -470
- package/lib/cjs/cli/types.js +0 -2
- package/lib/cjs/cli/utils.js +0 -639
- package/lib/cjs/common/ant-registry.js +0 -155
- package/lib/cjs/common/ant-versions.js +0 -93
- package/lib/cjs/common/ant.js +0 -1182
- package/lib/cjs/common/arweave.js +0 -27
- package/lib/cjs/common/contracts/ao-process.js +0 -224
- package/lib/cjs/common/error.js +0 -64
- package/lib/cjs/common/faucet.js +0 -150
- package/lib/cjs/common/hyperbeam/hb.js +0 -173
- package/lib/cjs/common/index.js +0 -42
- package/lib/cjs/common/io.js +0 -1423
- package/lib/cjs/common/logger.js +0 -83
- package/lib/cjs/common/loggers/winston.js +0 -68
- package/lib/cjs/common/marketplace.js +0 -731
- package/lib/cjs/common/turbo.js +0 -223
- package/lib/cjs/constants.js +0 -41
- package/lib/cjs/node/index.js +0 -39
- package/lib/cjs/package.json +0 -1
- package/lib/cjs/types/ant-registry.js +0 -2
- package/lib/cjs/types/ant.js +0 -168
- package/lib/cjs/types/common.js +0 -2
- package/lib/cjs/types/faucet.js +0 -2
- package/lib/cjs/types/index.js +0 -37
- package/lib/cjs/types/io.js +0 -51
- package/lib/cjs/types/token.js +0 -116
- package/lib/cjs/utils/ant.js +0 -108
- package/lib/cjs/utils/ao.js +0 -432
- package/lib/cjs/utils/arweave.js +0 -285
- package/lib/cjs/utils/base64.js +0 -62
- package/lib/cjs/utils/hash.js +0 -56
- package/lib/cjs/utils/index.js +0 -38
- package/lib/cjs/utils/processes.js +0 -173
- package/lib/cjs/utils/random.js +0 -30
- package/lib/cjs/utils/schema.js +0 -15
- package/lib/cjs/utils/url.js +0 -37
- package/lib/cjs/version.js +0 -20
- package/lib/cjs/web/index.js +0 -41
- package/lib/esm/common/ant-versions.js +0 -87
- package/lib/esm/common/arweave.js +0 -21
- package/lib/esm/common/contracts/ao-process.js +0 -220
- package/lib/esm/common/hyperbeam/hb.js +0 -169
- package/lib/esm/common/marketplace.js +0 -724
- package/lib/esm/common/turbo.js +0 -215
- package/lib/esm/node/index.js +0 -20
- package/lib/esm/utils/ao.js +0 -420
- package/lib/esm/utils/arweave.js +0 -271
- package/lib/esm/utils/processes.js +0 -167
- package/lib/esm/web/index.js +0 -20
- package/lib/types/common/ant-versions.d.ts +0 -39
- package/lib/types/common/arweave.d.ts +0 -17
- package/lib/types/common/contracts/ao-process.d.ts +0 -47
- package/lib/types/common/hyperbeam/hb.d.ts +0 -88
- package/lib/types/common/marketplace.d.ts +0 -568
- package/lib/types/common/turbo.d.ts +0 -61
- package/lib/types/node/index.d.ts +0 -20
- package/lib/types/utils/ao.d.ts +0 -80
- package/lib/types/utils/arweave.d.ts +0 -79
- package/lib/types/utils/processes.d.ts +0 -39
- 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
|
+
}
|