blindseal-sdk 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +109 -0
- package/dist/ix.d.ts +121 -0
- package/dist/ix.js +187 -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,63 @@
|
|
|
1
|
+
import { Connection, Keypair, PublicKey, type Signer } from '@solana/web3.js';
|
|
2
|
+
import { type Attestation, type DecodedPublicInputs } 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
|
+
/** Labeled top-level fields that must equal the committed public inputs. */
|
|
25
|
+
export declare const LABELED_FIELDS: readonly ["payloadHash", "policyRoot", "detectorVersion", "actionHash", "nullifier", "expirySlot"];
|
|
26
|
+
export type LabeledField = (typeof LABELED_FIELDS)[number];
|
|
27
|
+
/**
|
|
28
|
+
* Result of `verifyOffchain`.
|
|
29
|
+
* - `ok`: true only if the Groth16 proof verifies AND every labeled field equals its decoded value.
|
|
30
|
+
* - `reason`: present when `ok` is false — the wasm verifier's reason if the proof failed, otherwise
|
|
31
|
+
* `label mismatch: <field>[, <field>…]` (or the decode error if the blob is not 168 bytes).
|
|
32
|
+
* - `decoded`: the six fields decoded from `publicInputs[0]`; present whenever the blob parses,
|
|
33
|
+
* even when the proof or the labels fail. Hex fields are 0x-prefixed lower-case; `expirySlot`
|
|
34
|
+
* is a decimal u64 string.
|
|
35
|
+
* - `mismatches`: the labeled fields that differ from `decoded` (empty when none, or when the blob
|
|
36
|
+
* did not parse).
|
|
37
|
+
*/
|
|
38
|
+
export interface VerifyOffchainResult {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
reason?: string;
|
|
41
|
+
decoded?: DecodedPublicInputs;
|
|
42
|
+
mismatches: string[];
|
|
43
|
+
}
|
|
44
|
+
/** @deprecated Use `VerifyOffchainResult`. */
|
|
45
|
+
export type VerifyResult = VerifyOffchainResult;
|
|
46
|
+
/**
|
|
47
|
+
* Compare the six labeled top-level fields of an attestation to the decoded committed blob.
|
|
48
|
+
* Hex is compared case-insensitively with or without `0x`; `expirySlot` may be a number, bigint or
|
|
49
|
+
* decimal string. A missing or malformed label counts as a mismatch. Returns the differing field
|
|
50
|
+
* names in canonical order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function labelMismatches(att: Partial<Record<LabeledField, unknown>>, decoded: DecodedPublicInputs): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Off-chain verify via the same wasm the verify pages use, then cross-check the labels.
|
|
55
|
+
* 1. Decode `publicInputs[0]` into its six fields (see `decodePublicInputs`).
|
|
56
|
+
* 2. Groth16 verify of `proof.bytes` against `publicInputs[0]`, using the DECODED `detectorVersion`
|
|
57
|
+
* as the vkey hash (the committed one — same as the on-chain program, which checks the vkey it
|
|
58
|
+
* verified with equals the blob's). The proof therefore stands or falls on the blob alone.
|
|
59
|
+
* 3. Compare each labeled top-level field to the decoded value; any difference fails the result,
|
|
60
|
+
* because the proof binds only the blob — a tampered label would otherwise pass unnoticed.
|
|
61
|
+
* Does NOT check expiry against the current slot, nullifier consumption or the on-chain allowlists.
|
|
62
|
+
*/
|
|
63
|
+
export declare function verifyOffchain(att: Attestation): Promise<VerifyOffchainResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
/** Labeled top-level fields that must equal the committed public inputs. */
|
|
45
|
+
export const LABELED_FIELDS = ['payloadHash', 'policyRoot', 'detectorVersion', 'actionHash', 'nullifier', 'expirySlot'];
|
|
46
|
+
const normHex = (v) => '0x' + String(v ?? '').trim().replace(/^0x/i, '').toLowerCase();
|
|
47
|
+
const normSlot = (v) => {
|
|
48
|
+
if (typeof v === 'bigint')
|
|
49
|
+
return v.toString();
|
|
50
|
+
if (typeof v === 'number')
|
|
51
|
+
return Number.isInteger(v) && v >= 0 ? String(v) : null;
|
|
52
|
+
if (typeof v === 'string' && /^\d+$/.test(v.trim()))
|
|
53
|
+
return BigInt(v.trim()).toString();
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Compare the six labeled top-level fields of an attestation to the decoded committed blob.
|
|
58
|
+
* Hex is compared case-insensitively with or without `0x`; `expirySlot` may be a number, bigint or
|
|
59
|
+
* decimal string. A missing or malformed label counts as a mismatch. Returns the differing field
|
|
60
|
+
* names in canonical order.
|
|
61
|
+
*/
|
|
62
|
+
export function labelMismatches(att, decoded) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const k of LABELED_FIELDS) {
|
|
65
|
+
const same = k === 'expirySlot' ? normSlot(att.expirySlot) === decoded.expirySlot : normHex(att[k]) === decoded[k];
|
|
66
|
+
if (!same)
|
|
67
|
+
out.push(k);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
let wasmReady = null;
|
|
72
|
+
const loadWasm = () => (wasmReady ??= (async () => {
|
|
73
|
+
const wasm = await import('blindseal-verifier-wasm');
|
|
74
|
+
const { readFileSync } = await import('node:fs');
|
|
75
|
+
const req = createRequire(import.meta.url);
|
|
76
|
+
wasm.initSync({ module: readFileSync(req.resolve('blindseal-verifier-wasm/bg.wasm')) });
|
|
77
|
+
return wasm;
|
|
78
|
+
})().catch((e) => { wasmReady = null; throw e; }));
|
|
79
|
+
/**
|
|
80
|
+
* Off-chain verify via the same wasm the verify pages use, then cross-check the labels.
|
|
81
|
+
* 1. Decode `publicInputs[0]` into its six fields (see `decodePublicInputs`).
|
|
82
|
+
* 2. Groth16 verify of `proof.bytes` against `publicInputs[0]`, using the DECODED `detectorVersion`
|
|
83
|
+
* as the vkey hash (the committed one — same as the on-chain program, which checks the vkey it
|
|
84
|
+
* verified with equals the blob's). The proof therefore stands or falls on the blob alone.
|
|
85
|
+
* 3. Compare each labeled top-level field to the decoded value; any difference fails the result,
|
|
86
|
+
* because the proof binds only the blob — a tampered label would otherwise pass unnoticed.
|
|
87
|
+
* Does NOT check expiry against the current slot, nullifier consumption or the on-chain allowlists.
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyOffchain(att) {
|
|
90
|
+
const wasm = await loadWasm();
|
|
91
|
+
let decoded;
|
|
92
|
+
let decodeError;
|
|
93
|
+
try {
|
|
94
|
+
decoded = decodePublicInputs(String(att.publicInputs?.[0] ?? ''));
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
decodeError = e instanceof Error ? e.message : String(e);
|
|
98
|
+
}
|
|
99
|
+
const forWasm = decoded ? { ...att, detectorVersion: decoded.detectorVersion } : att;
|
|
100
|
+
const proof = JSON.parse(wasm.verify_attestation_json(JSON.stringify(forWasm)));
|
|
101
|
+
const mismatches = decoded ? labelMismatches(att, decoded) : [];
|
|
102
|
+
if (!proof.ok)
|
|
103
|
+
return { ok: false, reason: proof.reason ?? 'proof did not verify', decoded, mismatches };
|
|
104
|
+
if (!decoded)
|
|
105
|
+
return { ok: false, reason: decodeError ?? 'publicInputs did not decode', mismatches };
|
|
106
|
+
if (mismatches.length)
|
|
107
|
+
return { ok: false, reason: `label mismatch: ${mismatches.join(', ')}`, decoded, mismatches };
|
|
108
|
+
return { ok: true, decoded, mismatches };
|
|
109
|
+
}
|
package/dist/ix.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
export declare const PUBLIC_INPUTS_LEN = 168;
|
|
58
|
+
/** The six committed fields as 0x-prefixed lower-case hex; `expirySlot` is a decimal u64 string. */
|
|
59
|
+
export interface DecodedPublicInputs {
|
|
60
|
+
payloadHash: string;
|
|
61
|
+
policyRoot: string;
|
|
62
|
+
detectorVersion: string;
|
|
63
|
+
actionHash: string;
|
|
64
|
+
nullifier: string;
|
|
65
|
+
expirySlot: string;
|
|
66
|
+
}
|
|
67
|
+
/** Byte-level view of the same six fields (used by the instruction builders). */
|
|
68
|
+
export interface DecodedPublicInputsBytes {
|
|
69
|
+
payloadHash: Buffer;
|
|
70
|
+
policyRoot: Buffer;
|
|
71
|
+
detectorVersion: Buffer;
|
|
72
|
+
actionHash: Buffer;
|
|
73
|
+
nullifier: Buffer;
|
|
74
|
+
expirySlot: bigint;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decode the 168-byte committed blob into its six fields.
|
|
78
|
+
* - Given a hex string (`publicInputs[0]`, with or without `0x`), returns 0x-prefixed lower-case
|
|
79
|
+
* hex fields and `expirySlot` as a decimal string — the shape `verifyOffchain` reports as `decoded`.
|
|
80
|
+
* - Given raw bytes, returns Buffers and a bigint expiry (the form the instruction builders use).
|
|
81
|
+
* Throws if the input is not exactly 168 bytes (or not valid hex).
|
|
82
|
+
*/
|
|
83
|
+
export declare function decodePublicInputs(blobHex: string): DecodedPublicInputs;
|
|
84
|
+
export declare function decodePublicInputs(blob: Uint8Array): DecodedPublicInputsBytes;
|
|
85
|
+
export declare const discriminator: (name: string) => Buffer;
|
|
86
|
+
export declare function encodeActionArgs(a: ActionArgs): Buffer;
|
|
87
|
+
export interface VerifyAccounts {
|
|
88
|
+
payer: PublicKey;
|
|
89
|
+
/** Pass when the program runs in allowlist mode; omit otherwise. */
|
|
90
|
+
policyRoot?: PublicKey | null;
|
|
91
|
+
programId?: PublicKey;
|
|
92
|
+
}
|
|
93
|
+
/** `verify_and_consume(proof, public_inputs, action)` for composing into a gated transaction. */
|
|
94
|
+
export declare function buildVerifyIx(att: Attestation, action: ActionArgs, accounts: VerifyAccounts): TransactionInstruction;
|
|
95
|
+
/** `initialize(allowed_vkeys, allowlist_policies)` — creates the Config PDA; `authority` pays and becomes the authority. */
|
|
96
|
+
export declare function buildInitializeIx(authority: PublicKey, allowedVkeys: Uint8Array[], allowlistPolicies: boolean, programId?: PublicKey): TransactionInstruction;
|
|
97
|
+
/** `set_config(allowed_vkeys?, allowlist_policies?, paused?)` — each `undefined` leaves the field unchanged. */
|
|
98
|
+
export declare function buildSetConfigIx(authority: PublicKey, o: {
|
|
99
|
+
allowedVkeys?: Uint8Array[];
|
|
100
|
+
allowlistPolicies?: boolean;
|
|
101
|
+
paused?: boolean;
|
|
102
|
+
}, programId?: PublicKey): TransactionInstruction;
|
|
103
|
+
/** `register_policy_root(root)` — authority only; creates PDA(["policy", root]). */
|
|
104
|
+
export declare function buildRegisterPolicyRootIx(authority: PublicKey, root: Uint8Array, programId?: PublicKey): TransactionInstruction;
|
|
105
|
+
/** Decode the Config account (8 disc ‖ 32 authority ‖ vec<[u8;32]> ‖ bool allowlist ‖ bool paused ‖ u8 bump). */
|
|
106
|
+
export declare function decodeConfig(data: Uint8Array): {
|
|
107
|
+
authority: string;
|
|
108
|
+
allowedVkeys: string[];
|
|
109
|
+
allowlistPolicies: boolean;
|
|
110
|
+
paused: boolean;
|
|
111
|
+
bump: number;
|
|
112
|
+
};
|
|
113
|
+
/** `gated_transfer(proof, public_inputs, nonce, amount)`: the gate CPIs verify_and_consume with the
|
|
114
|
+
* action IT executes, then transfers `amount` lamports source → recipient in the same tx. */
|
|
115
|
+
export declare function buildGatedTransferIx(att: Attestation, accounts: {
|
|
116
|
+
source: PublicKey;
|
|
117
|
+
recipient: PublicKey;
|
|
118
|
+
policyRoot?: PublicKey | null;
|
|
119
|
+
gateId?: PublicKey;
|
|
120
|
+
verifyId?: PublicKey;
|
|
121
|
+
}): TransactionInstruction;
|
package/dist/ix.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
// ---- the committed public-input blob ----
|
|
47
|
+
// `publicInputs[0]` is the guest's public values in commit order (crates/guest/src/main.rs):
|
|
48
|
+
// [0..32) payloadHash [32..64) policyRoot [64..96) detectorVersion
|
|
49
|
+
// [96..128) actionHash [128..160) nullifier [160..168) expirySlot (u64 LE)
|
|
50
|
+
// 168 bytes = 336 hex chars. This is the ONLY data the proof binds; the six labeled top-level
|
|
51
|
+
// fields of attestation.json are convenience copies and must be checked against it.
|
|
52
|
+
export const PUBLIC_INPUTS_LEN = 168;
|
|
53
|
+
export function decodePublicInputs(blob) {
|
|
54
|
+
if (typeof blob === 'string') {
|
|
55
|
+
const h = blob.trim().replace(/^0x/i, '').toLowerCase();
|
|
56
|
+
if (!/^[0-9a-f]*$/.test(h) || h.length % 2 !== 0)
|
|
57
|
+
throw new Error('publicInputs is not a hex string');
|
|
58
|
+
const b = decodeBytes(Buffer.from(h, 'hex'));
|
|
59
|
+
const hx = (x) => '0x' + x.toString('hex');
|
|
60
|
+
return {
|
|
61
|
+
payloadHash: hx(b.payloadHash), policyRoot: hx(b.policyRoot), detectorVersion: hx(b.detectorVersion),
|
|
62
|
+
actionHash: hx(b.actionHash), nullifier: hx(b.nullifier), expirySlot: b.expirySlot.toString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return decodeBytes(blob);
|
|
66
|
+
}
|
|
67
|
+
function decodeBytes(blob) {
|
|
68
|
+
if (blob.length !== PUBLIC_INPUTS_LEN)
|
|
69
|
+
throw new Error(`publicInputs must be ${PUBLIC_INPUTS_LEN} bytes, got ${blob.length}`);
|
|
70
|
+
const b = Buffer.from(blob);
|
|
71
|
+
return {
|
|
72
|
+
payloadHash: b.subarray(0, 32), policyRoot: b.subarray(32, 64), detectorVersion: b.subarray(64, 96),
|
|
73
|
+
actionHash: b.subarray(96, 128), nullifier: b.subarray(128, 160), expirySlot: b.readBigUInt64LE(160),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ---- Anchor wire encoding ----
|
|
77
|
+
export const discriminator = (name) => createHash('sha256').update(`global:${name}`).digest().subarray(0, 8);
|
|
78
|
+
const vecU8 = (b) => { const len = Buffer.alloc(4); len.writeUInt32LE(b.length); return Buffer.concat([len, Buffer.from(b)]); };
|
|
79
|
+
export function encodeActionArgs(a) {
|
|
80
|
+
return Buffer.concat([
|
|
81
|
+
u16le(a.chainId), a.targetProgram.toBuffer(), a.sourceAuthority.toBuffer(), Buffer.from(a.instruction),
|
|
82
|
+
a.recipient.toBuffer(), a.mint.toBuffer(), u64le(a.amount), Buffer.from(a.nonce),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
/** `verify_and_consume(proof, public_inputs, action)` for composing into a gated transaction. */
|
|
86
|
+
export function buildVerifyIx(att, action, accounts) {
|
|
87
|
+
const programId = accounts.programId ?? PROGRAM_ID;
|
|
88
|
+
const proof = hex(att.proof.bytes);
|
|
89
|
+
const blob = hex(att.publicInputs[0]);
|
|
90
|
+
const { nullifier } = decodePublicInputs(blob);
|
|
91
|
+
const data = Buffer.concat([discriminator('verify_and_consume'), vecU8(proof), vecU8(blob), encodeActionArgs(action)]);
|
|
92
|
+
const policy = accounts.policyRoot === undefined ? policyRootPda(decodePublicInputs(blob).policyRoot, programId) : accounts.policyRoot;
|
|
93
|
+
return new TransactionInstruction({
|
|
94
|
+
programId,
|
|
95
|
+
keys: [
|
|
96
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: false },
|
|
97
|
+
// Anchor optional account: pass the program id itself for None.
|
|
98
|
+
{ pubkey: policy ?? programId, isSigner: false, isWritable: false },
|
|
99
|
+
{ pubkey: nullifierPda(nullifier, programId), isSigner: false, isWritable: true },
|
|
100
|
+
{ pubkey: accounts.payer, isSigner: true, isWritable: true },
|
|
101
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
102
|
+
],
|
|
103
|
+
data,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// ---- admin instructions (authority only) ----
|
|
107
|
+
const boolB = (b) => Buffer.from([b ? 1 : 0]);
|
|
108
|
+
const vecVkeys = (v) => { const len = Buffer.alloc(4); len.writeUInt32LE(v.length); return Buffer.concat([len, ...v.map((k) => Buffer.from(k))]); };
|
|
109
|
+
const option = (present, body) => (present ? Buffer.concat([Buffer.from([1]), body]) : Buffer.from([0]));
|
|
110
|
+
/** `initialize(allowed_vkeys, allowlist_policies)` — creates the Config PDA; `authority` pays and becomes the authority. */
|
|
111
|
+
export function buildInitializeIx(authority, allowedVkeys, allowlistPolicies, programId = PROGRAM_ID) {
|
|
112
|
+
return new TransactionInstruction({
|
|
113
|
+
programId,
|
|
114
|
+
keys: [
|
|
115
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: true },
|
|
116
|
+
{ pubkey: authority, isSigner: true, isWritable: true },
|
|
117
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
118
|
+
],
|
|
119
|
+
data: Buffer.concat([discriminator('initialize'), vecVkeys(allowedVkeys), boolB(allowlistPolicies)]),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** `set_config(allowed_vkeys?, allowlist_policies?, paused?)` — each `undefined` leaves the field unchanged. */
|
|
123
|
+
export function buildSetConfigIx(authority, o, programId = PROGRAM_ID) {
|
|
124
|
+
return new TransactionInstruction({
|
|
125
|
+
programId,
|
|
126
|
+
keys: [
|
|
127
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: true },
|
|
128
|
+
{ pubkey: authority, isSigner: true, isWritable: false },
|
|
129
|
+
],
|
|
130
|
+
data: Buffer.concat([
|
|
131
|
+
discriminator('set_config'),
|
|
132
|
+
option(o.allowedVkeys !== undefined, o.allowedVkeys ? vecVkeys(o.allowedVkeys) : Buffer.alloc(0)),
|
|
133
|
+
option(o.allowlistPolicies !== undefined, boolB(o.allowlistPolicies ?? false)),
|
|
134
|
+
option(o.paused !== undefined, boolB(o.paused ?? false)),
|
|
135
|
+
]),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** `register_policy_root(root)` — authority only; creates PDA(["policy", root]). */
|
|
139
|
+
export function buildRegisterPolicyRootIx(authority, root, programId = PROGRAM_ID) {
|
|
140
|
+
return new TransactionInstruction({
|
|
141
|
+
programId,
|
|
142
|
+
keys: [
|
|
143
|
+
{ pubkey: configPda(programId), isSigner: false, isWritable: false },
|
|
144
|
+
{ pubkey: policyRootPda(root, programId), isSigner: false, isWritable: true },
|
|
145
|
+
{ pubkey: authority, isSigner: true, isWritable: true },
|
|
146
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
147
|
+
],
|
|
148
|
+
data: Buffer.concat([discriminator('register_policy_root'), Buffer.from(root)]),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Decode the Config account (8 disc ‖ 32 authority ‖ vec<[u8;32]> ‖ bool allowlist ‖ bool paused ‖ u8 bump). */
|
|
152
|
+
export function decodeConfig(data) {
|
|
153
|
+
const b = Buffer.from(data);
|
|
154
|
+
const n = b.readUInt32LE(40);
|
|
155
|
+
const vkeys = [];
|
|
156
|
+
for (let i = 0; i < n; i++)
|
|
157
|
+
vkeys.push('0x' + b.subarray(44 + i * 32, 76 + i * 32).toString('hex'));
|
|
158
|
+
const off = 44 + n * 32;
|
|
159
|
+
return { authority: new PublicKey(b.subarray(8, 40)).toBase58(), allowedVkeys: vkeys, allowlistPolicies: b[off] === 1, paused: b[off + 1] === 1, bump: b[off + 2] };
|
|
160
|
+
}
|
|
161
|
+
// ---- transfer-gate (example gated program) ----
|
|
162
|
+
/** `gated_transfer(proof, public_inputs, nonce, amount)`: the gate CPIs verify_and_consume with the
|
|
163
|
+
* action IT executes, then transfers `amount` lamports source → recipient in the same tx. */
|
|
164
|
+
export function buildGatedTransferIx(att, accounts) {
|
|
165
|
+
const gateId = accounts.gateId ?? TRANSFER_GATE_ID;
|
|
166
|
+
const verifyId = accounts.verifyId ?? PROGRAM_ID;
|
|
167
|
+
const proof = hex(att.proof.bytes);
|
|
168
|
+
const blob = hex(att.publicInputs[0]);
|
|
169
|
+
const { nullifier, policyRoot } = decodePublicInputs(blob);
|
|
170
|
+
const amount = Buffer.alloc(8);
|
|
171
|
+
amount.writeBigUInt64LE(BigInt(att.action.amount));
|
|
172
|
+
const data = Buffer.concat([discriminator('gated_transfer'), vecU8(proof), vecU8(blob), hex(att.nonce), amount]);
|
|
173
|
+
const policy = accounts.policyRoot === undefined ? policyRootPda(policyRoot, verifyId) : accounts.policyRoot;
|
|
174
|
+
return new TransactionInstruction({
|
|
175
|
+
programId: gateId,
|
|
176
|
+
keys: [
|
|
177
|
+
{ pubkey: accounts.source, isSigner: true, isWritable: true },
|
|
178
|
+
{ pubkey: accounts.recipient, isSigner: false, isWritable: true },
|
|
179
|
+
{ pubkey: configPda(verifyId), isSigner: false, isWritable: false },
|
|
180
|
+
{ pubkey: policy ?? gateId, isSigner: false, isWritable: false }, // Anchor optional None = program id
|
|
181
|
+
{ pubkey: nullifierPda(nullifier, verifyId), isSigner: false, isWritable: true },
|
|
182
|
+
{ pubkey: verifyId, isSigner: false, isWritable: false },
|
|
183
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
184
|
+
],
|
|
185
|
+
data,
|
|
186
|
+
});
|
|
187
|
+
}
|