blindseal-sdk 0.0.0-stage → 0.5.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/LICENSE +22 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +52 -0
- package/dist/ix.d.ts +102 -0
- package/dist/ix.js +167 -0
- package/idl/blindseal_verify.json +546 -0
- package/idl/blindseal_verify.ts +552 -0
- package/idl/transfer_gate.json +82 -0
- package/idl/transfer_gate.ts +88 -0
- package/package.json +56 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VaultMCP contributors
|
|
4
|
+
Copyright (c) 2026 Blindseal contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Connection, Keypair, PublicKey, type Signer } from '@solana/web3.js';
|
|
2
|
+
import { type Attestation } from './ix.js';
|
|
3
|
+
export * from './ix.js';
|
|
4
|
+
export declare class WrongClusterError extends Error {
|
|
5
|
+
readonly got: string;
|
|
6
|
+
constructor(got: string);
|
|
7
|
+
}
|
|
8
|
+
export interface SubmitOptions {
|
|
9
|
+
wallet: Signer | Keypair;
|
|
10
|
+
rpcUrl?: string;
|
|
11
|
+
connection?: Connection;
|
|
12
|
+
policyRoot?: PublicKey | null;
|
|
13
|
+
computeUnits?: number;
|
|
14
|
+
/** Test hook only: skip the genesis guard (localnet). Never set in production. */
|
|
15
|
+
allowNonMainnet?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Submit an attestation directly (no gated program): verifies on-chain and consumes the nullifier.
|
|
19
|
+
* Refuses any cluster whose genesis hash is not mainnet-beta's — chainId 1 is the only chain.
|
|
20
|
+
*/
|
|
21
|
+
export declare function submit(att: Attestation, opts: SubmitOptions): Promise<string>;
|
|
22
|
+
/** Has this attestation's nullifier been consumed on chain? */
|
|
23
|
+
export declare function isConsumed(att: Attestation, conn: Connection, programId?: PublicKey): Promise<boolean>;
|
|
24
|
+
export interface VerifyResult {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
reason: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Off-chain Groth16 verify via the same wasm the verify pages use. */
|
|
29
|
+
export declare function verifyOffchain(att: Attestation): Promise<VerifyResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Blindseal SDK (BLINDSEAL.md §3.8). Mainnet is the only deploy target: chainId 1 = Solana
|
|
2
|
+
// mainnet, 2 is reserved. `submit()` refuses any RPC whose genesis hash is not mainnet-beta's.
|
|
3
|
+
// Everything encoding-related lives in ix.ts; this file adds the Node-dependent pieces.
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { ComputeBudgetProgram, Connection, Transaction } from '@solana/web3.js';
|
|
6
|
+
import { CHAIN_ID_SOLANA_MAINNET, DEFAULT_RPC_URL, MAINNET_GENESIS_HASH, PROGRAM_ID, VERIFY_CU_LIMIT, actionArgsFrom, buildVerifyIx, decodePublicInputs, hex, nullifierPda, } from './ix.js';
|
|
7
|
+
export * from './ix.js';
|
|
8
|
+
export class WrongClusterError extends Error {
|
|
9
|
+
got;
|
|
10
|
+
constructor(got) {
|
|
11
|
+
super(`RPC genesis hash ${got} is not Solana mainnet-beta (${MAINNET_GENESIS_HASH}); refusing to send`);
|
|
12
|
+
this.got = got;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Submit an attestation directly (no gated program): verifies on-chain and consumes the nullifier.
|
|
17
|
+
* Refuses any cluster whose genesis hash is not mainnet-beta's — chainId 1 is the only chain.
|
|
18
|
+
*/
|
|
19
|
+
export async function submit(att, opts) {
|
|
20
|
+
if (att.chainId !== CHAIN_ID_SOLANA_MAINNET)
|
|
21
|
+
throw new Error(`attestation chainId ${att.chainId} is not Solana mainnet (1)`);
|
|
22
|
+
const conn = opts.connection ?? new Connection(opts.rpcUrl ?? process.env.BLINDSEAL_RPC_URL ?? DEFAULT_RPC_URL, 'confirmed');
|
|
23
|
+
if (!opts.allowNonMainnet) {
|
|
24
|
+
const genesis = await conn.getGenesisHash();
|
|
25
|
+
if (genesis !== MAINNET_GENESIS_HASH)
|
|
26
|
+
throw new WrongClusterError(genesis);
|
|
27
|
+
}
|
|
28
|
+
const action = actionArgsFrom(att);
|
|
29
|
+
const ix = buildVerifyIx(att, action, { payer: opts.wallet.publicKey, policyRoot: opts.policyRoot });
|
|
30
|
+
const tx = new Transaction().add(ComputeBudgetProgram.setComputeUnitLimit({ units: opts.computeUnits ?? VERIFY_CU_LIMIT }), ix);
|
|
31
|
+
tx.feePayer = opts.wallet.publicKey;
|
|
32
|
+
tx.recentBlockhash = (await conn.getLatestBlockhash('confirmed')).blockhash;
|
|
33
|
+
tx.sign(opts.wallet);
|
|
34
|
+
const sig = await conn.sendRawTransaction(tx.serialize(), { skipPreflight: false });
|
|
35
|
+
await conn.confirmTransaction(sig, 'confirmed');
|
|
36
|
+
return sig;
|
|
37
|
+
}
|
|
38
|
+
/** Has this attestation's nullifier been consumed on chain? */
|
|
39
|
+
export async function isConsumed(att, conn, programId = PROGRAM_ID) {
|
|
40
|
+
const { nullifier } = decodePublicInputs(hex(att.publicInputs[0]));
|
|
41
|
+
const info = await conn.getAccountInfo(nullifierPda(nullifier, programId), 'confirmed');
|
|
42
|
+
return info !== null;
|
|
43
|
+
}
|
|
44
|
+
/** Off-chain Groth16 verify via the same wasm the verify pages use. */
|
|
45
|
+
export async function verifyOffchain(att) {
|
|
46
|
+
const wasm = await import('blindseal-verifier-wasm');
|
|
47
|
+
const { readFileSync } = await import('node:fs');
|
|
48
|
+
const req = createRequire(import.meta.url);
|
|
49
|
+
const bytes = readFileSync(req.resolve('blindseal-verifier-wasm/bg.wasm'));
|
|
50
|
+
wasm.initSync({ module: bytes });
|
|
51
|
+
return JSON.parse(wasm.verify_attestation_json(JSON.stringify(att)));
|
|
52
|
+
}
|
package/dist/ix.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
|
2
|
+
export declare const PROGRAM_ID: PublicKey;
|
|
3
|
+
export declare const TRANSFER_GATE_ID: PublicKey;
|
|
4
|
+
export declare const CHAIN_ID_SOLANA_MAINNET = 1;
|
|
5
|
+
/** Genesis hash of Solana mainnet-beta — the only cluster submit() will talk to. */
|
|
6
|
+
export declare const MAINNET_GENESIS_HASH = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
|
|
7
|
+
export declare const DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com";
|
|
8
|
+
/** Groth16 verify + nullifier creation; measured on localnet (docs/performance.md). */
|
|
9
|
+
export declare const VERIFY_CU_LIMIT = 400000;
|
|
10
|
+
export declare const ACTION_DOMAIN = "blindseal.action.v1";
|
|
11
|
+
export declare const NULLIFIER_DOMAIN = "blindseal.nullifier.v1";
|
|
12
|
+
export interface AttestationAction {
|
|
13
|
+
targetProgram: string;
|
|
14
|
+
sourceAuthority: string;
|
|
15
|
+
instruction: string;
|
|
16
|
+
recipient: string;
|
|
17
|
+
mint: string;
|
|
18
|
+
amount: string;
|
|
19
|
+
}
|
|
20
|
+
/** attestation.json as written by crates/prover / `blindseal attest`. */
|
|
21
|
+
export interface Attestation {
|
|
22
|
+
version: number;
|
|
23
|
+
decisionId?: string | null;
|
|
24
|
+
chainId: number;
|
|
25
|
+
payloadHash: string;
|
|
26
|
+
policyRoot: string;
|
|
27
|
+
detectorVersion: string;
|
|
28
|
+
actionHash: string;
|
|
29
|
+
nullifier: string;
|
|
30
|
+
expirySlot: number;
|
|
31
|
+
nonce: string;
|
|
32
|
+
action: AttestationAction;
|
|
33
|
+
publicInputs: [string];
|
|
34
|
+
proof: {
|
|
35
|
+
protocol: 'groth16';
|
|
36
|
+
curve: 'bn254';
|
|
37
|
+
bytes: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface ActionArgs {
|
|
41
|
+
chainId: number;
|
|
42
|
+
targetProgram: PublicKey;
|
|
43
|
+
sourceAuthority: PublicKey;
|
|
44
|
+
instruction: Uint8Array;
|
|
45
|
+
recipient: PublicKey;
|
|
46
|
+
mint: PublicKey;
|
|
47
|
+
amount: bigint;
|
|
48
|
+
nonce: Uint8Array;
|
|
49
|
+
}
|
|
50
|
+
export declare const hex: (s: string) => Buffer;
|
|
51
|
+
export declare function actionArgsFrom(att: Attestation): ActionArgs;
|
|
52
|
+
export declare const configPda: (programId?: PublicKey) => PublicKey;
|
|
53
|
+
export declare const policyRootPda: (root: Uint8Array, programId?: PublicKey) => PublicKey;
|
|
54
|
+
export declare const nullifierPda: (nullifier: Uint8Array, programId?: PublicKey) => PublicKey;
|
|
55
|
+
export declare function computeActionHash(a: ActionArgs, payloadHash: Uint8Array, policyRoot: Uint8Array, detectorVersion: Uint8Array, expirySlot: bigint): Buffer;
|
|
56
|
+
export declare const computeNullifier: (actionHash: Uint8Array) => Buffer;
|
|
57
|
+
/** Decode the 168-byte committed blob into its six fields. */
|
|
58
|
+
export declare function decodePublicInputs(blob: Uint8Array): {
|
|
59
|
+
payloadHash: Buffer;
|
|
60
|
+
policyRoot: Buffer;
|
|
61
|
+
detectorVersion: Buffer;
|
|
62
|
+
actionHash: Buffer;
|
|
63
|
+
nullifier: Buffer;
|
|
64
|
+
expirySlot: bigint;
|
|
65
|
+
};
|
|
66
|
+
export declare const discriminator: (name: string) => Buffer;
|
|
67
|
+
export declare function encodeActionArgs(a: ActionArgs): Buffer;
|
|
68
|
+
export interface VerifyAccounts {
|
|
69
|
+
payer: PublicKey;
|
|
70
|
+
/** Pass when the program runs in allowlist mode; omit otherwise. */
|
|
71
|
+
policyRoot?: PublicKey | null;
|
|
72
|
+
programId?: PublicKey;
|
|
73
|
+
}
|
|
74
|
+
/** `verify_and_consume(proof, public_inputs, action)` for composing into a gated transaction. */
|
|
75
|
+
export declare function buildVerifyIx(att: Attestation, action: ActionArgs, accounts: VerifyAccounts): TransactionInstruction;
|
|
76
|
+
/** `initialize(allowed_vkeys, allowlist_policies)` — creates the Config PDA; `authority` pays and becomes the authority. */
|
|
77
|
+
export declare function buildInitializeIx(authority: PublicKey, allowedVkeys: Uint8Array[], allowlistPolicies: boolean, programId?: PublicKey): TransactionInstruction;
|
|
78
|
+
/** `set_config(allowed_vkeys?, allowlist_policies?, paused?)` — each `undefined` leaves the field unchanged. */
|
|
79
|
+
export declare function buildSetConfigIx(authority: PublicKey, o: {
|
|
80
|
+
allowedVkeys?: Uint8Array[];
|
|
81
|
+
allowlistPolicies?: boolean;
|
|
82
|
+
paused?: boolean;
|
|
83
|
+
}, programId?: PublicKey): TransactionInstruction;
|
|
84
|
+
/** `register_policy_root(root)` — authority only; creates PDA(["policy", root]). */
|
|
85
|
+
export declare function buildRegisterPolicyRootIx(authority: PublicKey, root: Uint8Array, programId?: PublicKey): TransactionInstruction;
|
|
86
|
+
/** Decode the Config account (8 disc ‖ 32 authority ‖ vec<[u8;32]> ‖ bool allowlist ‖ bool paused ‖ u8 bump). */
|
|
87
|
+
export declare function decodeConfig(data: Uint8Array): {
|
|
88
|
+
authority: string;
|
|
89
|
+
allowedVkeys: string[];
|
|
90
|
+
allowlistPolicies: boolean;
|
|
91
|
+
paused: boolean;
|
|
92
|
+
bump: number;
|
|
93
|
+
};
|
|
94
|
+
/** `gated_transfer(proof, public_inputs, nonce, amount)`: the gate CPIs verify_and_consume with the
|
|
95
|
+
* action IT executes, then transfers `amount` lamports source → recipient in the same tx. */
|
|
96
|
+
export declare function buildGatedTransferIx(att: Attestation, accounts: {
|
|
97
|
+
source: PublicKey;
|
|
98
|
+
recipient: PublicKey;
|
|
99
|
+
policyRoot?: PublicKey | null;
|
|
100
|
+
gateId?: PublicKey;
|
|
101
|
+
verifyId?: PublicKey;
|
|
102
|
+
}): TransactionInstruction;
|
package/dist/ix.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Pure, environment-agnostic part of the SDK: constants, types, PDAs, hashing (byte-for-byte
|
|
2
|
+
// detector-core / on-chain), Anchor wire encoding and every instruction builder. Imported by the
|
|
3
|
+
// localnet suite (CommonJS) and by index.ts (adds submit / verifyOffchain, which need Node APIs).
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
|
|
6
|
+
export const PROGRAM_ID = new PublicKey('merZNYKpKwwX4CGgzvma3jPvLfrMTkZcb1cSXUKANcb');
|
|
7
|
+
export const TRANSFER_GATE_ID = new PublicKey('ErAS5x2oUky3aCJPasSKNaf8USVE9of5vzHHCPWwSEVG');
|
|
8
|
+
export const CHAIN_ID_SOLANA_MAINNET = 1;
|
|
9
|
+
/** Genesis hash of Solana mainnet-beta — the only cluster submit() will talk to. */
|
|
10
|
+
export const MAINNET_GENESIS_HASH = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d';
|
|
11
|
+
export const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
|
|
12
|
+
/** Groth16 verify + nullifier creation; measured on localnet (docs/performance.md). */
|
|
13
|
+
export const VERIFY_CU_LIMIT = 400_000;
|
|
14
|
+
export const ACTION_DOMAIN = 'blindseal.action.v1';
|
|
15
|
+
export const NULLIFIER_DOMAIN = 'blindseal.nullifier.v1';
|
|
16
|
+
export const hex = (s) => Buffer.from(s.replace(/^0x/, ''), 'hex');
|
|
17
|
+
const pk = (s) => (s ? new PublicKey(s) : PublicKey.default);
|
|
18
|
+
export function actionArgsFrom(att) {
|
|
19
|
+
return {
|
|
20
|
+
chainId: att.chainId,
|
|
21
|
+
targetProgram: pk(att.action.targetProgram),
|
|
22
|
+
sourceAuthority: pk(att.action.sourceAuthority),
|
|
23
|
+
instruction: hex(att.action.instruction),
|
|
24
|
+
recipient: pk(att.action.recipient),
|
|
25
|
+
mint: pk(att.action.mint),
|
|
26
|
+
amount: BigInt(att.action.amount),
|
|
27
|
+
nonce: hex(att.nonce),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ---- PDAs ----
|
|
31
|
+
export const configPda = (programId = PROGRAM_ID) => PublicKey.findProgramAddressSync([Buffer.from('config')], programId)[0];
|
|
32
|
+
export const policyRootPda = (root, programId = PROGRAM_ID) => PublicKey.findProgramAddressSync([Buffer.from('policy'), Buffer.from(root)], programId)[0];
|
|
33
|
+
export const nullifierPda = (nullifier, programId = PROGRAM_ID) => PublicKey.findProgramAddressSync([Buffer.from('nullifier'), Buffer.from(nullifier)], programId)[0];
|
|
34
|
+
// ---- hashing (byte-for-byte detector-core / on-chain) ----
|
|
35
|
+
const u16le = (n) => { const b = Buffer.alloc(2); b.writeUInt16LE(n); return b; };
|
|
36
|
+
const u64le = (n) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(n); return b; };
|
|
37
|
+
export function computeActionHash(a, payloadHash, policyRoot, detectorVersion, expirySlot) {
|
|
38
|
+
return createHash('sha256')
|
|
39
|
+
.update(ACTION_DOMAIN).update(u16le(a.chainId))
|
|
40
|
+
.update(a.targetProgram.toBytes()).update(a.sourceAuthority.toBytes()).update(a.instruction)
|
|
41
|
+
.update(a.recipient.toBytes()).update(a.mint.toBytes()).update(u64le(a.amount))
|
|
42
|
+
.update(payloadHash).update(policyRoot).update(detectorVersion).update(u64le(expirySlot)).update(a.nonce)
|
|
43
|
+
.digest();
|
|
44
|
+
}
|
|
45
|
+
export const computeNullifier = (actionHash) => createHash('sha256').update(NULLIFIER_DOMAIN).update(actionHash).digest();
|
|
46
|
+
/** Decode the 168-byte committed blob into its six fields. */
|
|
47
|
+
export function decodePublicInputs(blob) {
|
|
48
|
+
if (blob.length !== 168)
|
|
49
|
+
throw new Error(`publicInputs must be 168 bytes, got ${blob.length}`);
|
|
50
|
+
const b = Buffer.from(blob);
|
|
51
|
+
return {
|
|
52
|
+
payloadHash: b.subarray(0, 32), policyRoot: b.subarray(32, 64), detectorVersion: b.subarray(64, 96),
|
|
53
|
+
actionHash: b.subarray(96, 128), nullifier: b.subarray(128, 160), expirySlot: b.readBigUInt64LE(160),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ---- Anchor wire encoding ----
|
|
57
|
+
export const discriminator = (name) => createHash('sha256').update(`global:${name}`).digest().subarray(0, 8);
|
|
58
|
+
const vecU8 = (b) => { const len = Buffer.alloc(4); len.writeUInt32LE(b.length); return Buffer.concat([len, Buffer.from(b)]); };
|
|
59
|
+
export function encodeActionArgs(a) {
|
|
60
|
+
return Buffer.concat([
|
|
61
|
+
u16le(a.chainId), a.targetProgram.toBuffer(), a.sourceAuthority.toBuffer(), Buffer.from(a.instruction),
|
|
62
|
+
a.recipient.toBuffer(), a.mint.toBuffer(), u64le(a.amount), Buffer.from(a.nonce),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
/** `verify_and_consume(proof, public_inputs, action)` for composing into a gated transaction. */
|
|
66
|
+
export function buildVerifyIx(att, action, accounts) {
|
|
67
|
+
const programId = accounts.programId ?? PROGRAM_ID;
|
|
68
|
+
const proof = hex(att.proof.bytes);
|
|
69
|
+
const blob = hex(att.publicInputs[0]);
|
|
70
|
+
const { nullifier } = decodePublicInputs(blob);
|
|
71
|
+
const data = Buffer.concat([discriminator('verify_and_consume'), vecU8(proof), vecU8(blob), encodeActionArgs(action)]);
|
|
72
|
+
const policy = accounts.policyRoot === undefined ? policyRootPda(decodePublicInputs(blob).policyRoot, programId) : accounts.policyRoot;
|
|
73
|
+
return new TransactionInstruction({
|
|
74
|
+
programId,
|
|
75
|
+
keys: [
|
|
76
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: false },
|
|
77
|
+
// Anchor optional account: pass the program id itself for None.
|
|
78
|
+
{ pubkey: policy ?? programId, isSigner: false, isWritable: false },
|
|
79
|
+
{ pubkey: nullifierPda(nullifier, programId), isSigner: false, isWritable: true },
|
|
80
|
+
{ pubkey: accounts.payer, isSigner: true, isWritable: true },
|
|
81
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
82
|
+
],
|
|
83
|
+
data,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ---- admin instructions (authority only) ----
|
|
87
|
+
const boolB = (b) => Buffer.from([b ? 1 : 0]);
|
|
88
|
+
const vecVkeys = (v) => { const len = Buffer.alloc(4); len.writeUInt32LE(v.length); return Buffer.concat([len, ...v.map((k) => Buffer.from(k))]); };
|
|
89
|
+
const option = (present, body) => (present ? Buffer.concat([Buffer.from([1]), body]) : Buffer.from([0]));
|
|
90
|
+
/** `initialize(allowed_vkeys, allowlist_policies)` — creates the Config PDA; `authority` pays and becomes the authority. */
|
|
91
|
+
export function buildInitializeIx(authority, allowedVkeys, allowlistPolicies, programId = PROGRAM_ID) {
|
|
92
|
+
return new TransactionInstruction({
|
|
93
|
+
programId,
|
|
94
|
+
keys: [
|
|
95
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: true },
|
|
96
|
+
{ pubkey: authority, isSigner: true, isWritable: true },
|
|
97
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
98
|
+
],
|
|
99
|
+
data: Buffer.concat([discriminator('initialize'), vecVkeys(allowedVkeys), boolB(allowlistPolicies)]),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** `set_config(allowed_vkeys?, allowlist_policies?, paused?)` — each `undefined` leaves the field unchanged. */
|
|
103
|
+
export function buildSetConfigIx(authority, o, programId = PROGRAM_ID) {
|
|
104
|
+
return new TransactionInstruction({
|
|
105
|
+
programId,
|
|
106
|
+
keys: [
|
|
107
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: true },
|
|
108
|
+
{ pubkey: authority, isSigner: true, isWritable: false },
|
|
109
|
+
],
|
|
110
|
+
data: Buffer.concat([
|
|
111
|
+
discriminator('set_config'),
|
|
112
|
+
option(o.allowedVkeys !== undefined, o.allowedVkeys ? vecVkeys(o.allowedVkeys) : Buffer.alloc(0)),
|
|
113
|
+
option(o.allowlistPolicies !== undefined, boolB(o.allowlistPolicies ?? false)),
|
|
114
|
+
option(o.paused !== undefined, boolB(o.paused ?? false)),
|
|
115
|
+
]),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** `register_policy_root(root)` — authority only; creates PDA(["policy", root]). */
|
|
119
|
+
export function buildRegisterPolicyRootIx(authority, root, programId = PROGRAM_ID) {
|
|
120
|
+
return new TransactionInstruction({
|
|
121
|
+
programId,
|
|
122
|
+
keys: [
|
|
123
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: false },
|
|
124
|
+
{ pubkey: policyRootPda(root, programId), isSigner: false, isWritable: true },
|
|
125
|
+
{ pubkey: authority, isSigner: true, isWritable: true },
|
|
126
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
127
|
+
],
|
|
128
|
+
data: Buffer.concat([discriminator('register_policy_root'), Buffer.from(root)]),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Decode the Config account (8 disc ‖ 32 authority ‖ vec<[u8;32]> ‖ bool allowlist ‖ bool paused ‖ u8 bump). */
|
|
132
|
+
export function decodeConfig(data) {
|
|
133
|
+
const b = Buffer.from(data);
|
|
134
|
+
const n = b.readUInt32LE(40);
|
|
135
|
+
const vkeys = [];
|
|
136
|
+
for (let i = 0; i < n; i++)
|
|
137
|
+
vkeys.push('0x' + b.subarray(44 + i * 32, 76 + i * 32).toString('hex'));
|
|
138
|
+
const off = 44 + n * 32;
|
|
139
|
+
return { authority: new PublicKey(b.subarray(8, 40)).toBase58(), allowedVkeys: vkeys, allowlistPolicies: b[off] === 1, paused: b[off + 1] === 1, bump: b[off + 2] };
|
|
140
|
+
}
|
|
141
|
+
// ---- transfer-gate (example gated program) ----
|
|
142
|
+
/** `gated_transfer(proof, public_inputs, nonce, amount)`: the gate CPIs verify_and_consume with the
|
|
143
|
+
* action IT executes, then transfers `amount` lamports source → recipient in the same tx. */
|
|
144
|
+
export function buildGatedTransferIx(att, accounts) {
|
|
145
|
+
const gateId = accounts.gateId ?? TRANSFER_GATE_ID;
|
|
146
|
+
const verifyId = accounts.verifyId ?? PROGRAM_ID;
|
|
147
|
+
const proof = hex(att.proof.bytes);
|
|
148
|
+
const blob = hex(att.publicInputs[0]);
|
|
149
|
+
const { nullifier, policyRoot } = decodePublicInputs(blob);
|
|
150
|
+
const amount = Buffer.alloc(8);
|
|
151
|
+
amount.writeBigUInt64LE(BigInt(att.action.amount));
|
|
152
|
+
const data = Buffer.concat([discriminator('gated_transfer'), vecU8(proof), vecU8(blob), hex(att.nonce), amount]);
|
|
153
|
+
const policy = accounts.policyRoot === undefined ? policyRootPda(policyRoot, verifyId) : accounts.policyRoot;
|
|
154
|
+
return new TransactionInstruction({
|
|
155
|
+
programId: gateId,
|
|
156
|
+
keys: [
|
|
157
|
+
{ pubkey: accounts.source, isSigner: true, isWritable: true },
|
|
158
|
+
{ pubkey: accounts.recipient, isSigner: false, isWritable: true },
|
|
159
|
+
{ pubkey: configPda(verifyId), isSigner: false, isWritable: false },
|
|
160
|
+
{ pubkey: policy ?? gateId, isSigner: false, isWritable: false }, // Anchor optional None = program id
|
|
161
|
+
{ pubkey: nullifierPda(nullifier, verifyId), isSigner: false, isWritable: true },
|
|
162
|
+
{ pubkey: verifyId, isSigner: false, isWritable: false },
|
|
163
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
164
|
+
],
|
|
165
|
+
data,
|
|
166
|
+
});
|
|
167
|
+
}
|