@x1id/resolve 0.23.0 → 0.44.0
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 +97 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +6 -1
- package/dist/agent.d.ts +184 -8
- package/dist/agent.js +351 -3
- package/dist/agentPolicy.d.ts +238 -0
- package/dist/agentPolicy.js +364 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +25 -0
- package/dist/checkout.d.ts +216 -0
- package/dist/checkout.js +327 -0
- package/dist/credentials.d.ts +466 -0
- package/dist/credentials.js +669 -0
- package/dist/gate.d.ts +94 -4
- package/dist/gate.js +206 -7
- package/dist/gateClient.d.ts +12 -0
- package/dist/giftLink.d.ts +272 -0
- package/dist/giftLink.js +452 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +32 -0
- package/dist/loginButton.d.ts +69 -0
- package/dist/loginButton.js +207 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +8 -0
- package/dist/messaging.d.ts +439 -0
- package/dist/messaging.js +979 -0
- package/dist/nsGate.d.ts +98 -0
- package/dist/nsGate.js +172 -0
- package/dist/oidc.d.ts +272 -0
- package/dist/oidc.js +641 -0
- package/dist/pay.d.ts +513 -0
- package/dist/pay.js +756 -0
- package/dist/payment.d.ts +9 -2
- package/dist/payment.js +9 -2
- package/dist/recovery.d.ts +186 -0
- package/dist/recovery.js +276 -0
- package/dist/recoveryNft.d.ts +183 -0
- package/dist/recoveryNft.js +291 -0
- package/dist/shield.d.ts +373 -0
- package/dist/shield.js +695 -0
- package/dist/subscription.d.ts +439 -0
- package/dist/subscription.js +642 -0
- package/dist/textRecords.d.ts +52 -6
- package/dist/textRecords.js +80 -8
- package/dist/types.d.ts +3 -0
- package/dist/x402Receipt.d.ts +246 -0
- package/dist/x402Receipt.js +374 -0
- package/package.json +18 -1
- package/schema/agent-manifest.json +91 -8
package/dist/shield.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shield: confidential zXNT / zUSDC.X on X1 (docs/specs/shield-confidential-tokens.md).
|
|
3
|
+
*
|
|
4
|
+
* What this module gives an integrator, with no dependencies and no proofs:
|
|
5
|
+
*
|
|
6
|
+
* - the Shield addresses, derived exactly like `programs/x1-shield`:
|
|
7
|
+
* z-mint `["z-mint", backing]`, `ShieldMint` `["shield-mint", z_mint]`, the
|
|
8
|
+
* reserve (the ShieldMint PDA's associated token account of the backing
|
|
9
|
+
* mint) and a holder's Shield account (their Token-2022 ATA of the z-mint);
|
|
10
|
+
* - the `shield` / `unshield` instruction builders (`buildShieldIx`,
|
|
11
|
+
* `buildUnshieldIx`);
|
|
12
|
+
* - decoders for a `ShieldMint` record and a Token-2022 account's
|
|
13
|
+
* `ConfidentialTransferAccount` extension, plus the "ready to receive
|
|
14
|
+
* privately" rule (spec §7);
|
|
15
|
+
* - {@link resolveShieldRecipient}: `@name` → the address to pay under the
|
|
16
|
+
* one payment rule (verified X1 record, else the current authority), its
|
|
17
|
+
* Shield account and whether it is ready, honouring the `x1.shield = off`
|
|
18
|
+
* opt-out text record and refusing reserved (treasury-held) names;
|
|
19
|
+
* - payment receipts (`x1receipt1…`): a strict bech32m codec, and
|
|
20
|
+
* {@link verifyShieldReceipt}, which finds the payment's proof data on chain
|
|
21
|
+
* and hands it to a commitment checker.
|
|
22
|
+
*
|
|
23
|
+
* Proof generation and the elliptic-curve check of a receipt stay in the
|
|
24
|
+
* separate `shield-proofs` WASM (spec §7: "so the SDK stays light"); this
|
|
25
|
+
* module takes that check as a function (`checkCommitments`), e.g.
|
|
26
|
+
* `(r, kind, data) => wasm.verifyReceiptAgainstProof(r, kind, data)`.
|
|
27
|
+
*
|
|
28
|
+
* PDAs come through an injected {@link PdaFinder}, like every other builder in
|
|
29
|
+
* this package: it never hand-rolls the ed25519 on-curve check.
|
|
30
|
+
*/
|
|
31
|
+
import { encodeBase58, decodeBase58, decodeBase58_32 } from "./base58.js";
|
|
32
|
+
import { makeAccountReader, nftHolder, parseHandleAccount, } from "./accounts.js";
|
|
33
|
+
import { fetchRecords, liveRecords } from "./records.js";
|
|
34
|
+
import { fetchTextRecords, findTextRecord } from "./textRecords.js";
|
|
35
|
+
import { fetchConfigTreasury } from "./register.js";
|
|
36
|
+
import { paymentAddressFor } from "./payment.js";
|
|
37
|
+
import { ResolveError } from "./types.js";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Constants
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/** `programs/x1-shield` as deployed on X1 TESTNET (S1). Mainnet is not
|
|
42
|
+
* deployed yet (owner gate S5); a mainnet deploy may use its own id, so
|
|
43
|
+
* integrators should take the id from their network config. */
|
|
44
|
+
export const SHIELD_PROGRAM_ID_TESTNET = "7Kqs3Nu82F1NCoUgnKEBUyW2uiftRXQjc7vJ3D2Zdzxo";
|
|
45
|
+
export const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
46
|
+
/** Classic SPL Token (p-token on X1) — the wrapped-native mint's program. */
|
|
47
|
+
export const SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
48
|
+
export const ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
49
|
+
export const ZK_ELGAMAL_PROOF_PROGRAM_ID = "ZkE1Gama1Proof11111111111111111111111111111";
|
|
50
|
+
/** Wrapped-native mint: the backing of zXNT. */
|
|
51
|
+
export const NATIVE_MINT = "So11111111111111111111111111111111111111112";
|
|
52
|
+
/** USDC.X on X1 mainnet: the backing of zUSDC.X there (spec §3). */
|
|
53
|
+
export const USDCX_MINT_MAINNET = "B69chRzqzDCmdB5WYB8NRu5Yv5ZA95ABiZcdzCgGm9Tq";
|
|
54
|
+
export const Z_MINT_SEED = "z-mint";
|
|
55
|
+
export const SHIELD_MINT_SEED = "shield-mint";
|
|
56
|
+
/** `sha256("global:shield")[0..8]`. */
|
|
57
|
+
export const SHIELD_DISCRIMINATOR = Uint8Array.from([220, 198, 253, 246, 231, 84, 147, 98]);
|
|
58
|
+
/** `sha256("global:unshield")[0..8]`. */
|
|
59
|
+
export const UNSHIELD_DISCRIMINATOR = Uint8Array.from([21, 228, 55, 24, 194, 10, 21, 22]);
|
|
60
|
+
/** `sha256("account:ShieldMint")[0..8]`. */
|
|
61
|
+
export const SHIELD_MINT_ACCOUNT_DISCRIMINATOR = Uint8Array.from([183, 22, 172, 202, 204, 78, 142, 136]);
|
|
62
|
+
/** Token-2022 caps one confidential Deposit at 2^48 − 1 base units, so one
|
|
63
|
+
* `shield` above that reverts (x1-shield FBL-0008): split it. */
|
|
64
|
+
export const MAX_SHIELD_AMOUNT = (1n << 48n) - 1n;
|
|
65
|
+
/** The X1ID text record a name sets to `off` to opt out of being offered for
|
|
66
|
+
* private payments (spec §7). */
|
|
67
|
+
export const SHIELD_OPT_OUT_TEXT_KEY = "x1.shield";
|
|
68
|
+
const TE = new TextEncoder();
|
|
69
|
+
function toBytes32(v, what) {
|
|
70
|
+
if (typeof v === "string") {
|
|
71
|
+
const b = decodeBase58_32(v);
|
|
72
|
+
if (!b)
|
|
73
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
74
|
+
return b;
|
|
75
|
+
}
|
|
76
|
+
if (v.length !== 32)
|
|
77
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
78
|
+
return v;
|
|
79
|
+
}
|
|
80
|
+
function toBase58(v, what) {
|
|
81
|
+
return encodeBase58(toBytes32(v, what));
|
|
82
|
+
}
|
|
83
|
+
function u64le(n) {
|
|
84
|
+
if (n < 0n || n > 0xffffffffffffffffn)
|
|
85
|
+
throw new Error("amount must fit in a u64");
|
|
86
|
+
const b = new Uint8Array(8);
|
|
87
|
+
new DataView(b.buffer).setBigUint64(0, n, true);
|
|
88
|
+
return b;
|
|
89
|
+
}
|
|
90
|
+
function readU64LE(d, o) {
|
|
91
|
+
return new DataView(d.buffer, d.byteOffset, d.byteLength).getBigUint64(o, true);
|
|
92
|
+
}
|
|
93
|
+
function b64ToBytes(s) {
|
|
94
|
+
const bin = atob(s);
|
|
95
|
+
const out = new Uint8Array(bin.length);
|
|
96
|
+
for (let i = 0; i < bin.length; i++)
|
|
97
|
+
out[i] = bin.charCodeAt(i);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// PDAs
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/** The canonical z-mint of `backingMint`: `["z-mint", backing]`. */
|
|
104
|
+
export async function deriveZMint(find, programId, backingMint) {
|
|
105
|
+
return find([TE.encode(Z_MINT_SEED), toBytes32(backingMint, "backingMint")], toBase58(programId, "programId"));
|
|
106
|
+
}
|
|
107
|
+
/** A z-mint's `ShieldMint` record (its mint authority and the reserve's
|
|
108
|
+
* owner): `["shield-mint", z_mint]`. */
|
|
109
|
+
export async function deriveShieldMint(find, programId, zMint) {
|
|
110
|
+
return find([TE.encode(SHIELD_MINT_SEED), toBytes32(zMint, "zMint")], toBase58(programId, "programId"));
|
|
111
|
+
}
|
|
112
|
+
/** Associated token account of `owner` for `mint` under `tokenProgram`. */
|
|
113
|
+
export async function deriveAssociatedTokenAccount(find, owner, mint, tokenProgram) {
|
|
114
|
+
return find([toBytes32(owner, "owner"), toBytes32(tokenProgram, "tokenProgram"), toBytes32(mint, "mint")], ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
115
|
+
}
|
|
116
|
+
/** A holder's Shield account for `zMint`: their Token-2022 ATA of it. */
|
|
117
|
+
export function deriveShieldAccount(find, owner, zMint) {
|
|
118
|
+
return deriveAssociatedTokenAccount(find, owner, zMint, TOKEN_2022_PROGRAM_ID);
|
|
119
|
+
}
|
|
120
|
+
export async function deriveShieldAccounts(find, p) {
|
|
121
|
+
const zMint = await deriveZMint(find, p.programId, p.backingMint);
|
|
122
|
+
const shieldMint = await deriveShieldMint(find, p.programId, zMint);
|
|
123
|
+
const reserve = await deriveAssociatedTokenAccount(find, shieldMint, p.backingMint, p.backingTokenProgram);
|
|
124
|
+
if (p.owner === undefined)
|
|
125
|
+
return { zMint, shieldMint, reserve };
|
|
126
|
+
const [zAccount, backingAccount] = await Promise.all([
|
|
127
|
+
deriveShieldAccount(find, p.owner, zMint),
|
|
128
|
+
deriveAssociatedTokenAccount(find, p.owner, p.backingMint, p.backingTokenProgram),
|
|
129
|
+
]);
|
|
130
|
+
return { zMint, shieldMint, reserve, zAccount, backingAccount };
|
|
131
|
+
}
|
|
132
|
+
const k = (pubkey, what, isSigner, isWritable) => ({
|
|
133
|
+
pubkey: toBase58(pubkey, what),
|
|
134
|
+
isSigner,
|
|
135
|
+
isWritable,
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* `shield(amount)`: move backing into the reserve, mint exactly what arrived
|
|
139
|
+
* to the owner's Shield account and deposit it into its confidential PENDING
|
|
140
|
+
* balance. The owner's Shield account must already be configured for
|
|
141
|
+
* confidential balances. For zXNT, `backingAccount` is the owner's
|
|
142
|
+
* wrapped-native ATA, funded in the same transaction (create idempotent →
|
|
143
|
+
* system transfer → SyncNative → shield → CloseAccount; see
|
|
144
|
+
* programs/x1-shield/src/lib.rs "Client sequences").
|
|
145
|
+
*/
|
|
146
|
+
export function buildShieldIx(p) {
|
|
147
|
+
return {
|
|
148
|
+
programId: toBase58(p.programId, "programId"),
|
|
149
|
+
keys: [
|
|
150
|
+
k(p.owner, "owner", true, false),
|
|
151
|
+
k(p.shieldMint, "shieldMint", false, true),
|
|
152
|
+
k(p.zMint, "zMint", false, true),
|
|
153
|
+
k(p.backingMint, "backingMint", false, false),
|
|
154
|
+
k(p.backingAccount, "backingAccount", false, true),
|
|
155
|
+
k(p.reserve, "reserve", false, true),
|
|
156
|
+
k(p.zAccount, "zAccount", false, true),
|
|
157
|
+
k(p.backingTokenProgram, "backingTokenProgram", false, false),
|
|
158
|
+
k(TOKEN_2022_PROGRAM_ID, "zTokenProgram", false, false),
|
|
159
|
+
],
|
|
160
|
+
data: concatBytes(SHIELD_DISCRIMINATOR, u64le(p.amount)),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* `unshield(amount)`: burn `amount` from the owner's PUBLIC z-balance (put
|
|
165
|
+
* there by a Token-2022 confidential Withdraw earlier in the same
|
|
166
|
+
* transaction), verify the burn, release exactly that backing to
|
|
167
|
+
* `backingAccount`. Note the account order differs from `shield`.
|
|
168
|
+
*/
|
|
169
|
+
export function buildUnshieldIx(p) {
|
|
170
|
+
return {
|
|
171
|
+
programId: toBase58(p.programId, "programId"),
|
|
172
|
+
keys: [
|
|
173
|
+
k(p.owner, "owner", true, false),
|
|
174
|
+
k(p.shieldMint, "shieldMint", false, true),
|
|
175
|
+
k(p.zMint, "zMint", false, true),
|
|
176
|
+
k(p.backingMint, "backingMint", false, false),
|
|
177
|
+
k(p.zAccount, "zAccount", false, true),
|
|
178
|
+
k(p.reserve, "reserve", false, true),
|
|
179
|
+
k(p.backingAccount, "backingAccount", false, true),
|
|
180
|
+
k(p.backingTokenProgram, "backingTokenProgram", false, false),
|
|
181
|
+
k(TOKEN_2022_PROGRAM_ID, "zTokenProgram", false, false),
|
|
182
|
+
],
|
|
183
|
+
data: concatBytes(UNSHIELD_DISCRIMINATOR, u64le(p.amount)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function concatBytes(...parts) {
|
|
187
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
188
|
+
let o = 0;
|
|
189
|
+
for (const p of parts) {
|
|
190
|
+
out.set(p, o);
|
|
191
|
+
o += p.length;
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
/** `disc(8) z_mint(32) backing_mint(32) reserve(32) backing_token_program(32)
|
|
196
|
+
* decimals(1) shielded_supply(8) bump(1) z_mint_bump(1)`. Null when the bytes
|
|
197
|
+
* are not a ShieldMint. */
|
|
198
|
+
export function parseShieldMint(data) {
|
|
199
|
+
if (data.length < 147)
|
|
200
|
+
return null;
|
|
201
|
+
for (let i = 0; i < 8; i++)
|
|
202
|
+
if (data[i] !== SHIELD_MINT_ACCOUNT_DISCRIMINATOR[i])
|
|
203
|
+
return null;
|
|
204
|
+
const pk = (o) => encodeBase58(data.slice(o, o + 32));
|
|
205
|
+
return {
|
|
206
|
+
zMint: pk(8),
|
|
207
|
+
backingMint: pk(40),
|
|
208
|
+
reserve: pk(72),
|
|
209
|
+
backingTokenProgram: pk(104),
|
|
210
|
+
decimals: data[136],
|
|
211
|
+
shieldedSupply: readU64LE(data, 137),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** spl-token-2022 base `Account` length; extensions follow. */
|
|
215
|
+
const BASE_ACCOUNT_LEN = 165;
|
|
216
|
+
const ACCOUNT_TYPE_ACCOUNT = 2;
|
|
217
|
+
/** `ExtensionType::ConfidentialTransferAccount`. */
|
|
218
|
+
export const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT = 5;
|
|
219
|
+
const CT_ACCOUNT_LEN = 295;
|
|
220
|
+
/** Decode a Token-2022 token account and its confidential extension. Null
|
|
221
|
+
* when the bytes are not a token account. Pure. */
|
|
222
|
+
export function decodeConfidentialTokenAccount(data) {
|
|
223
|
+
if (data.length < BASE_ACCOUNT_LEN)
|
|
224
|
+
return null;
|
|
225
|
+
const mint = encodeBase58(data.slice(0, 32));
|
|
226
|
+
const owner = encodeBase58(data.slice(32, 64));
|
|
227
|
+
const amount = readU64LE(data, 64);
|
|
228
|
+
let ct = null;
|
|
229
|
+
if (data.length > BASE_ACCOUNT_LEN) {
|
|
230
|
+
if (data[BASE_ACCOUNT_LEN] !== ACCOUNT_TYPE_ACCOUNT)
|
|
231
|
+
return null;
|
|
232
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
233
|
+
let o = BASE_ACCOUNT_LEN + 1;
|
|
234
|
+
while (o + 4 <= data.length) {
|
|
235
|
+
const type = dv.getUint16(o, true);
|
|
236
|
+
const len = dv.getUint16(o + 2, true);
|
|
237
|
+
const start = o + 4;
|
|
238
|
+
if (type === 0 || start + len > data.length)
|
|
239
|
+
break; // Uninitialized / padding
|
|
240
|
+
if (type === EXT_CONFIDENTIAL_TRANSFER_ACCOUNT && len >= CT_ACCOUNT_LEN) {
|
|
241
|
+
let p = start;
|
|
242
|
+
const take = (n) => {
|
|
243
|
+
const v = data.slice(p, p + n);
|
|
244
|
+
p += n;
|
|
245
|
+
return v;
|
|
246
|
+
};
|
|
247
|
+
const approved = take(1)[0] === 1;
|
|
248
|
+
const elgamalPubkey = take(32);
|
|
249
|
+
const pendingLo = take(64);
|
|
250
|
+
const pendingHi = take(64);
|
|
251
|
+
const available = take(64);
|
|
252
|
+
const decryptable = take(36);
|
|
253
|
+
const allowConfidentialCredits = take(1)[0] === 1;
|
|
254
|
+
const allowNonConfidentialCredits = take(1)[0] === 1;
|
|
255
|
+
ct = {
|
|
256
|
+
approved,
|
|
257
|
+
elgamalPubkey,
|
|
258
|
+
pendingLo,
|
|
259
|
+
pendingHi,
|
|
260
|
+
available,
|
|
261
|
+
decryptable,
|
|
262
|
+
allowConfidentialCredits,
|
|
263
|
+
allowNonConfidentialCredits,
|
|
264
|
+
pendingCreditCounter: readU64LE(data, p),
|
|
265
|
+
maxPendingCreditCounter: readU64LE(data, p + 8),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
o = start + len;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { mint, owner, amount, ct };
|
|
272
|
+
}
|
|
273
|
+
/** True for the opt-out text record's value (`off`, any case/whitespace). */
|
|
274
|
+
export function isShieldOptOut(value) {
|
|
275
|
+
return (value ?? "").trim().toLowerCase() === "off";
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Judge readiness to receive privately (spec §7) from the account at the
|
|
279
|
+
* derived Shield address: `account` is `{ data, owner }` as read from chain
|
|
280
|
+
* (null = no account). Pure.
|
|
281
|
+
*/
|
|
282
|
+
export function shieldReadiness(account, expected, optedOut = false) {
|
|
283
|
+
if (optedOut)
|
|
284
|
+
return { kind: "opted-out" };
|
|
285
|
+
if (!account)
|
|
286
|
+
return { kind: "no-account" };
|
|
287
|
+
if (account.owner !== TOKEN_2022_PROGRAM_ID)
|
|
288
|
+
return { kind: "mismatch" };
|
|
289
|
+
const acct = decodeConfidentialTokenAccount(account.data);
|
|
290
|
+
if (!acct || acct.owner !== expected.owner || acct.mint !== expected.zMint)
|
|
291
|
+
return { kind: "mismatch" };
|
|
292
|
+
if (!acct.ct)
|
|
293
|
+
return { kind: "not-configured" };
|
|
294
|
+
if (!acct.ct.approved || !acct.ct.allowConfidentialCredits)
|
|
295
|
+
return { kind: "not-accepting" };
|
|
296
|
+
return { kind: "ready", elgamalPubkey: acct.ct.elgamalPubkey };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolve an @name for a private payment in `zMint` (spec §7):
|
|
300
|
+
*
|
|
301
|
+
* 1. the name's payment address under the one rule (`paymentAddressFor`): a
|
|
302
|
+
* live VERIFIED X1 record, else the current authority (`Handle.owner`, or
|
|
303
|
+
* the pNFT holder of a tokenized name);
|
|
304
|
+
* 2. a reserved name (its payment target is the registry treasury) is
|
|
305
|
+
* REFUSED — `ResolveError("reserved")`;
|
|
306
|
+
* 3. the recipient's Shield account (their Token-2022 ATA of `zMint`) is read
|
|
307
|
+
* from chain: `ataReady` iff it is configured for confidential balances;
|
|
308
|
+
* 4. a live `x1.shield = off` text record makes the name not ready
|
|
309
|
+
* (`readiness.kind === "opted-out"`), however its account looks.
|
|
310
|
+
*
|
|
311
|
+
* No X1ID record is needed to receive privately; readiness is chain state.
|
|
312
|
+
*/
|
|
313
|
+
export async function resolveShieldRecipient(name, zMint, config) {
|
|
314
|
+
const reader = makeAccountReader(config.rpcUrl, config.fetchImpl);
|
|
315
|
+
const rpc = reader.rpc;
|
|
316
|
+
const zMintB58 = toBase58(zMint, "zMint");
|
|
317
|
+
const programBytes = toBytes32(config.handleProgramId, "handleProgramId");
|
|
318
|
+
const programB58 = encodeBase58(programBytes);
|
|
319
|
+
const canonical = config.wasm.normalizeHandle(name.trim().replace(/^@/, ""));
|
|
320
|
+
if (!canonical)
|
|
321
|
+
throw new ResolveError("invalid-handle", `"${name}" is not a valid handle`, name);
|
|
322
|
+
const derived = config.wasm.deriveHandleAccount(canonical, programBytes);
|
|
323
|
+
if (!derived)
|
|
324
|
+
throw new ResolveError("invalid-handle", `"${name}" is not a valid handle`, name);
|
|
325
|
+
const handleAccount = encodeBase58(derived);
|
|
326
|
+
const info = await reader.accountInfo(handleAccount);
|
|
327
|
+
if (!info || info.owner !== programB58)
|
|
328
|
+
throw new ResolveError("not-found", `@${canonical} is not registered`, name);
|
|
329
|
+
const handle = parseHandleAccount(info.data);
|
|
330
|
+
if (!handle)
|
|
331
|
+
throw new ResolveError("rpc-error", `@${canonical} returned a malformed account`, name);
|
|
332
|
+
const authority = handle.nftMint === null
|
|
333
|
+
? encodeBase58(handle.owner)
|
|
334
|
+
: (await nftHolder(reader, config.wasm, canonical, handle.nftMint, name)).address;
|
|
335
|
+
const treasury = config.treasury ??
|
|
336
|
+
(await fetchConfigTreasury(rpc, await config.find([TE.encode("config")], programB58))) ??
|
|
337
|
+
null;
|
|
338
|
+
const reserved = () => new ResolveError("reserved", `@${canonical} is reserved by X1ID and can't receive payments`, name);
|
|
339
|
+
if (treasury !== null && authority === treasury)
|
|
340
|
+
throw reserved();
|
|
341
|
+
const records = liveRecords(await fetchRecords(rpc, programB58, handleAccount, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch));
|
|
342
|
+
const payment = paymentAddressFor(records, authority);
|
|
343
|
+
if (!payment)
|
|
344
|
+
throw new ResolveError("not-found", `@${canonical} has no payment address right now`, name);
|
|
345
|
+
if (treasury !== null && payment.address === treasury)
|
|
346
|
+
throw reserved();
|
|
347
|
+
// holderEpoch: a native tokenized sale must stale the seller's opt-out.
|
|
348
|
+
// findTextRecord: byte-exact key, live only, never a BOM look-alike.
|
|
349
|
+
const texts = await fetchTextRecords(rpc, programB58, handleAccount, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch);
|
|
350
|
+
const optedOut = isShieldOptOut(findTextRecord(texts, SHIELD_OPT_OUT_TEXT_KEY)?.text);
|
|
351
|
+
const zAccount = await deriveShieldAccount(config.find, payment.address, zMintB58);
|
|
352
|
+
const acct = await reader.accountInfo(zAccount);
|
|
353
|
+
const readiness = shieldReadiness(acct, { owner: payment.address, zMint: zMintB58 }, optedOut);
|
|
354
|
+
return {
|
|
355
|
+
name: canonical,
|
|
356
|
+
handleAccount,
|
|
357
|
+
address: payment.address,
|
|
358
|
+
basis: payment.basis,
|
|
359
|
+
zAccount,
|
|
360
|
+
ataReady: readiness.kind === "ready",
|
|
361
|
+
readiness,
|
|
362
|
+
elgamalPubkey: readiness.kind === "ready" ? readiness.elgamalPubkey : null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// bech32m (BIP-350) — the receipt / view-key string format
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
369
|
+
const BECH32M_CONST = 0x2bc830a3;
|
|
370
|
+
function polymod(values) {
|
|
371
|
+
const G = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
372
|
+
let chk = 1;
|
|
373
|
+
for (const v of values) {
|
|
374
|
+
const top = chk >>> 25;
|
|
375
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
376
|
+
for (let i = 0; i < 5; i++)
|
|
377
|
+
if ((top >>> i) & 1)
|
|
378
|
+
chk ^= G[i];
|
|
379
|
+
}
|
|
380
|
+
return chk >>> 0;
|
|
381
|
+
}
|
|
382
|
+
function hrpExpand(hrp) {
|
|
383
|
+
const out = [];
|
|
384
|
+
for (let i = 0; i < hrp.length; i++)
|
|
385
|
+
out.push(hrp.charCodeAt(i) >> 5);
|
|
386
|
+
out.push(0);
|
|
387
|
+
for (let i = 0; i < hrp.length; i++)
|
|
388
|
+
out.push(hrp.charCodeAt(i) & 31);
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
function convertBits(data, from, to, pad) {
|
|
392
|
+
let acc = 0;
|
|
393
|
+
let bits = 0;
|
|
394
|
+
const out = [];
|
|
395
|
+
const maxv = (1 << to) - 1;
|
|
396
|
+
for (let i = 0; i < data.length; i++) {
|
|
397
|
+
const v = data[i];
|
|
398
|
+
if (v < 0 || v >> from !== 0)
|
|
399
|
+
return null;
|
|
400
|
+
acc = (acc << from) | v;
|
|
401
|
+
bits += from;
|
|
402
|
+
while (bits >= to) {
|
|
403
|
+
bits -= to;
|
|
404
|
+
out.push((acc >> bits) & maxv);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (pad) {
|
|
408
|
+
if (bits > 0)
|
|
409
|
+
out.push((acc << (to - bits)) & maxv);
|
|
410
|
+
}
|
|
411
|
+
else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
/** bech32m-encode `bytes` under `hrp` (no 90-char limit: receipts are long). */
|
|
417
|
+
export function bech32mEncode(hrp, bytes) {
|
|
418
|
+
const data = convertBits(bytes, 8, 5, true);
|
|
419
|
+
const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
|
|
420
|
+
const mod = polymod(values) ^ BECH32M_CONST;
|
|
421
|
+
const checksum = Array.from({ length: 6 }, (_, i) => (mod >>> (5 * (5 - i))) & 31);
|
|
422
|
+
return `${hrp}1${[...data, ...checksum].map((d) => CHARSET[d]).join("")}`;
|
|
423
|
+
}
|
|
424
|
+
/** Strict bech32m decode: right checksum constant (not BIP-173 bech32), no
|
|
425
|
+
* mixed case, the expected `hrp`. Null on any failure. */
|
|
426
|
+
export function bech32mDecode(s, hrp) {
|
|
427
|
+
if (s.length > 1023)
|
|
428
|
+
return null;
|
|
429
|
+
if (s !== s.toLowerCase() && s !== s.toUpperCase())
|
|
430
|
+
return null;
|
|
431
|
+
const lower = s.toLowerCase();
|
|
432
|
+
const sep = lower.lastIndexOf("1");
|
|
433
|
+
if (sep < 1 || sep + 7 > lower.length)
|
|
434
|
+
return null;
|
|
435
|
+
if (lower.slice(0, sep) !== hrp)
|
|
436
|
+
return null;
|
|
437
|
+
const data = [];
|
|
438
|
+
for (const c of lower.slice(sep + 1)) {
|
|
439
|
+
const v = CHARSET.indexOf(c);
|
|
440
|
+
if (v < 0)
|
|
441
|
+
return null;
|
|
442
|
+
data.push(v);
|
|
443
|
+
}
|
|
444
|
+
if (polymod([...hrpExpand(hrp), ...data]) !== BECH32M_CONST)
|
|
445
|
+
return null;
|
|
446
|
+
const bytes = convertBits(data.slice(0, -6), 5, 8, false);
|
|
447
|
+
return bytes ? Uint8Array.from(bytes) : null;
|
|
448
|
+
}
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Payment receipts (spec §5)
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
export const SHIELD_RECEIPT_HRP = "x1receipt";
|
|
453
|
+
const RECEIPT_VERSION = 1;
|
|
454
|
+
const RECEIPT_LEN = 1 + 64 + 32 + 8 + 32 + 32;
|
|
455
|
+
export function encodeShieldReceipt(r) {
|
|
456
|
+
const sig = decodeBase58(r.txSignature, 64);
|
|
457
|
+
if (!sig)
|
|
458
|
+
throw new Error("txSignature must be a 64-byte base58 signature");
|
|
459
|
+
if (r.openingLo.length !== 32 || r.openingHi.length !== 32)
|
|
460
|
+
throw new Error("openings must be 32 bytes each");
|
|
461
|
+
return bech32mEncode(SHIELD_RECEIPT_HRP, concatBytes(Uint8Array.of(RECEIPT_VERSION), sig, toBytes32(r.mint, "mint"), u64le(r.amount), r.openingLo, r.openingHi));
|
|
462
|
+
}
|
|
463
|
+
/** Decode a receipt string. Null when it isn't a valid v1 receipt. */
|
|
464
|
+
export function decodeShieldReceipt(s) {
|
|
465
|
+
const raw = bech32mDecode(s.trim(), SHIELD_RECEIPT_HRP);
|
|
466
|
+
if (!raw || raw.length !== RECEIPT_LEN || raw[0] !== RECEIPT_VERSION)
|
|
467
|
+
return null;
|
|
468
|
+
return {
|
|
469
|
+
txSignature: encodeBase58(raw.slice(1, 65)),
|
|
470
|
+
mint: encodeBase58(raw.slice(65, 97)),
|
|
471
|
+
amount: readU64LE(raw, 97),
|
|
472
|
+
openingLo: raw.slice(105, 137),
|
|
473
|
+
openingHi: raw.slice(137, 169),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Pull a receipt out of what someone pasted or scanned: the bare string,
|
|
478
|
+
* with whitespace or line breaks, or a `…/receipt?r=…` link. Returns the
|
|
479
|
+
* normalized lowercase receipt only when it decodes; null otherwise.
|
|
480
|
+
*/
|
|
481
|
+
export function extractShieldReceipt(input) {
|
|
482
|
+
let s = input.trim();
|
|
483
|
+
if (!s)
|
|
484
|
+
return null;
|
|
485
|
+
if (/^https?:\/\//i.test(s)) {
|
|
486
|
+
try {
|
|
487
|
+
s = new URL(s).searchParams.get("r") ?? "";
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
s = s.replace(/\s+/g, "");
|
|
494
|
+
if (s !== s.toLowerCase() && s !== s.toUpperCase())
|
|
495
|
+
return null;
|
|
496
|
+
s = s.toLowerCase();
|
|
497
|
+
return decodeShieldReceipt(s) ? s : null;
|
|
498
|
+
}
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Payment lookup + receipt verification (S-2: proofs live in the staging txs)
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
/** `ProofType` of the two proofs that carry a transfer's amount commitments. */
|
|
503
|
+
export const PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY = 12;
|
|
504
|
+
export const PROOF_TYPE_BATCHED_RANGE_U128 = 7;
|
|
505
|
+
async function getTx(rpc, sig) {
|
|
506
|
+
return (await rpc("getTransaction", [
|
|
507
|
+
sig,
|
|
508
|
+
{ encoding: "json", commitment: "confirmed", maxSupportedTransactionVersion: 0 },
|
|
509
|
+
]));
|
|
510
|
+
}
|
|
511
|
+
function ixsOf(tx) {
|
|
512
|
+
const keys = tx.transaction.message.accountKeys;
|
|
513
|
+
return tx.transaction.message.instructions.map((ix) => ({
|
|
514
|
+
program: keys[ix.programIdIndex],
|
|
515
|
+
accounts: ix.accounts.map((i) => keys[i]),
|
|
516
|
+
data: decodeBase58(ix.data) ?? new Uint8Array(0),
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
/** Successful transactions that touched `account` at or before `slot`. */
|
|
520
|
+
async function txsTouching(rpc, account, slot, exclude) {
|
|
521
|
+
const sigs = ((await rpc("getSignaturesForAddress", [account, { limit: 25, commitment: "confirmed" }])) ?? []);
|
|
522
|
+
const out = [];
|
|
523
|
+
for (const s of sigs) {
|
|
524
|
+
if (s.err || s.slot > slot || s.signature === exclude)
|
|
525
|
+
continue;
|
|
526
|
+
const t = await getTx(rpc, s.signature);
|
|
527
|
+
if (t && !t.meta?.err)
|
|
528
|
+
out.push(t);
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
/** Token-2022 `TransferInstructionData` (no fee): [27, 7] ‖ new decryptable
|
|
533
|
+
* available 36 ‖ auditor ciphertext lo 64 ‖ hi 64 ‖ equality, validity,
|
|
534
|
+
* range proof instruction offsets (i8 each). */
|
|
535
|
+
const TRANSFER_DATA_LEN = 2 + 36 + 64 + 64 + 3;
|
|
536
|
+
const TRANSFER_COMMITMENT_LO = 2 + 36;
|
|
537
|
+
const TRANSFER_COMMITMENT_HI = 2 + 36 + 64;
|
|
538
|
+
const TRANSFER_OFFSETS = 2 + 36 + 64 + 64;
|
|
539
|
+
/** Offsets inside `BatchedGroupedCiphertext3HandlesValidityProofData`:
|
|
540
|
+
* context = first(source) pubkey 32 ‖ second(destination) pubkey 32 ‖
|
|
541
|
+
* third(auditor) pubkey 32 ‖ grouped ciphertext lo 128 ‖ hi 128 (each =
|
|
542
|
+
* commitment 32 ‖ handle×3), then the proof. */
|
|
543
|
+
const VALIDITY_CONTEXT_LEN = 96 + 128 + 128;
|
|
544
|
+
const VALIDITY_COMMITMENT_LO = 96;
|
|
545
|
+
const VALIDITY_COMMITMENT_HI = 96 + 128;
|
|
546
|
+
function bytesEqual(a, b) {
|
|
547
|
+
if (a.length !== b.length)
|
|
548
|
+
return false;
|
|
549
|
+
let d = 0;
|
|
550
|
+
for (let i = 0; i < a.length; i++)
|
|
551
|
+
d |= a[i] ^ b[i];
|
|
552
|
+
return d === 0;
|
|
553
|
+
}
|
|
554
|
+
/** Does this validity proof carry exactly the Transfer's amount commitments?
|
|
555
|
+
* The payer controls the context account and can verify any number of
|
|
556
|
+
* other proofs into it, so a proof found by address alone proves nothing
|
|
557
|
+
* about this payment (Sentinel FBL-0009). */
|
|
558
|
+
function provesThisTransfer(proof, lo, hi) {
|
|
559
|
+
return (proof.length >= VALIDITY_CONTEXT_LEN &&
|
|
560
|
+
bytesEqual(proof.subarray(VALIDITY_COMMITMENT_LO, VALIDITY_COMMITMENT_LO + 32), lo) &&
|
|
561
|
+
bytesEqual(proof.subarray(VALIDITY_COMMITMENT_HI, VALIDITY_COMMITMENT_HI + 32), hi));
|
|
562
|
+
}
|
|
563
|
+
const isInlineValidity = (ix) => ix.program === ZK_ELGAMAL_PROOF_PROGRAM_ID &&
|
|
564
|
+
ix.data[0] === PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY &&
|
|
565
|
+
ix.data.length > 1 + VALIDITY_CONTEXT_LEN;
|
|
566
|
+
/**
|
|
567
|
+
* Find a confidential payment and the proof data its amount lives in. The
|
|
568
|
+
* Transfer's accounts are decoded from its proof-offset bytes (an inline
|
|
569
|
+
* proof in the same transaction, or a context-state account plus the
|
|
570
|
+
* Instructions sysvar). For a context account the proof bytes come from the
|
|
571
|
+
* staging transaction that verified the 3-handle validity proof into it
|
|
572
|
+
* (crates/shield-proofs README, "S-2"), which depends on the RPC node still
|
|
573
|
+
* holding that transaction. Either way a proof is used only when its
|
|
574
|
+
* commitments equal the ones in the Transfer instruction, which Token-2022
|
|
575
|
+
* checked against the proof it consumed. Null when `txSignature` isn't a
|
|
576
|
+
* successful Token-2022 confidential transfer.
|
|
577
|
+
*/
|
|
578
|
+
export async function findShieldPayment(rpc, txSignature) {
|
|
579
|
+
const tx = await getTx(rpc, txSignature);
|
|
580
|
+
if (!tx || tx.meta?.err)
|
|
581
|
+
return null;
|
|
582
|
+
const ixs = ixsOf(tx);
|
|
583
|
+
const at = ixs.findIndex((ix) => ix.program === TOKEN_2022_PROGRAM_ID && ix.data[0] === 27 && ix.data[1] === 7);
|
|
584
|
+
const transfer = ixs[at];
|
|
585
|
+
if (!transfer || transfer.data.length !== TRANSFER_DATA_LEN)
|
|
586
|
+
return null;
|
|
587
|
+
const off = (i) => (transfer.data[TRANSFER_OFFSETS + i] << 24) >> 24; // i8
|
|
588
|
+
const [eqOff, valOff, rangeOff] = [off(0), off(1), off(2)];
|
|
589
|
+
// source, mint, destination, [Instructions sysvar if any proof is inline],
|
|
590
|
+
// then a context account for each proof that isn't inline, then authority
|
|
591
|
+
let i = 3;
|
|
592
|
+
if (eqOff !== 0 || valOff !== 0 || rangeOff !== 0)
|
|
593
|
+
i++;
|
|
594
|
+
if (eqOff === 0)
|
|
595
|
+
i++;
|
|
596
|
+
const ctxVal = valOff === 0 ? transfer.accounts[i++] : undefined;
|
|
597
|
+
if (rangeOff === 0)
|
|
598
|
+
i++;
|
|
599
|
+
const sender = transfer.accounts[i];
|
|
600
|
+
const [src, mint, dst] = transfer.accounts;
|
|
601
|
+
if (!src || !mint || !dst || !sender || (valOff === 0 && !ctxVal))
|
|
602
|
+
return null;
|
|
603
|
+
const lo = transfer.data.slice(TRANSFER_COMMITMENT_LO, TRANSFER_COMMITMENT_LO + 32);
|
|
604
|
+
const hi = transfer.data.slice(TRANSFER_COMMITMENT_HI, TRANSFER_COMMITMENT_HI + 32);
|
|
605
|
+
let validityProof = null;
|
|
606
|
+
if (valOff !== 0) {
|
|
607
|
+
const v = ixs[at + valOff];
|
|
608
|
+
if (v && isInlineValidity(v) && provesThisTransfer(v.data.subarray(1), lo, hi))
|
|
609
|
+
validityProof = v.data.slice(1);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
for (const t of await txsTouching(rpc, ctxVal, tx.slot, txSignature)) {
|
|
613
|
+
const v = ixsOf(t).find((ix) => isInlineValidity(ix) && ix.accounts[0] === ctxVal && provesThisTransfer(ix.data.subarray(1), lo, hi));
|
|
614
|
+
if (v) {
|
|
615
|
+
validityProof = v.data.slice(1);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
txSignature,
|
|
622
|
+
slot: tx.slot,
|
|
623
|
+
blockTime: tx.blockTime,
|
|
624
|
+
zMint: mint,
|
|
625
|
+
sender,
|
|
626
|
+
sourceAccount: src,
|
|
627
|
+
destinationAccount: dst,
|
|
628
|
+
amountCommitmentLo: lo,
|
|
629
|
+
amountCommitmentHi: hi,
|
|
630
|
+
validityProof,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The amount ciphertexts of a payment as seen by one party (`"source"` or
|
|
635
|
+
* `"destination"`): 64-byte ElGamal ciphertexts `commitment ‖ that party's
|
|
636
|
+
* handle` for the amount's lo (16-bit) and hi (32-bit) parts. Decrypt with
|
|
637
|
+
* the party's keys as `lo + hi·2^16` (shield-proofs `decryptPending(lo, hi)`
|
|
638
|
+
* does exactly that). Also returns the party's ElGamal key from the proof so
|
|
639
|
+
* a caller can check it is theirs. Null on malformed data.
|
|
640
|
+
*/
|
|
641
|
+
export function paymentAmountCiphertexts(validityProof, party) {
|
|
642
|
+
if (validityProof.length < VALIDITY_CONTEXT_LEN)
|
|
643
|
+
return null;
|
|
644
|
+
const h = party === "source" ? 0 : 1;
|
|
645
|
+
const grouped = (start) => concatBytes(validityProof.slice(start, start + 32), validityProof.slice(start + 32 + 32 * h, start + 64 + 32 * h));
|
|
646
|
+
return {
|
|
647
|
+
lo: grouped(96),
|
|
648
|
+
hi: grouped(96 + 128),
|
|
649
|
+
elgamalPubkey: validityProof.slice(32 * h, 32 * h + 32),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Verify a receipt against the chain: decode it, find the payment
|
|
654
|
+
* ({@link findShieldPayment}), check the z-mint, and ask `checkCommitments`
|
|
655
|
+
* (the shield-proofs WASM's `verifyReceiptAgainstProof`) whether the
|
|
656
|
+
* receipt's amount and openings reproduce the commitments in the payment's
|
|
657
|
+
* own validity proof: the one whose commitments equal the Transfer
|
|
658
|
+
* instruction's (Sentinel FBL-0009). Anyone can run it; it needs no keys.
|
|
659
|
+
*/
|
|
660
|
+
export async function verifyShieldReceipt(receipt, opts) {
|
|
661
|
+
const r = decodeShieldReceipt(receipt);
|
|
662
|
+
if (!r)
|
|
663
|
+
return { ok: false, reason: "malformed" };
|
|
664
|
+
const tx = await getTx(opts.rpc, r.txSignature);
|
|
665
|
+
if (!tx || tx.meta?.err)
|
|
666
|
+
return { ok: false, reason: "not-found" };
|
|
667
|
+
const p = await findShieldPayment(opts.rpc, r.txSignature);
|
|
668
|
+
if (!p)
|
|
669
|
+
return { ok: false, reason: "not-a-payment" };
|
|
670
|
+
if (p.zMint !== r.mint)
|
|
671
|
+
return { ok: false, reason: "wrong-token" };
|
|
672
|
+
if (!p.validityProof || !provesThisTransfer(p.validityProof, p.amountCommitmentLo, p.amountCommitmentHi)) {
|
|
673
|
+
return { ok: false, reason: "no-proof-history" };
|
|
674
|
+
}
|
|
675
|
+
let ok = false;
|
|
676
|
+
try {
|
|
677
|
+
ok = await opts.checkCommitments(receipt.trim().toLowerCase(), PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY, p.validityProof);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
ok = false;
|
|
681
|
+
}
|
|
682
|
+
if (!ok)
|
|
683
|
+
return { ok: false, reason: "mismatch" };
|
|
684
|
+
return {
|
|
685
|
+
ok: true,
|
|
686
|
+
amount: r.amount,
|
|
687
|
+
zMint: p.zMint,
|
|
688
|
+
txSignature: p.txSignature,
|
|
689
|
+
blockTime: p.blockTime,
|
|
690
|
+
sender: p.sender,
|
|
691
|
+
recipientAccount: p.destinationAccount,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** Re-exported for callers that want to read account bytes the same way. */
|
|
695
|
+
export { b64ToBytes as shieldBase64ToBytes };
|