@voidifydao/sdk 1.0.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/dist/cli/config/command.d.ts +2 -0
- package/dist/cli/config/command.js +91 -0
- package/dist/cli/config/init.d.ts +3 -0
- package/dist/cli/config/init.js +88 -0
- package/dist/cli/config/keypair.d.ts +4 -0
- package/dist/cli/config/keypair.js +35 -0
- package/dist/cli/config/loader.d.ts +11 -0
- package/dist/cli/config/loader.js +65 -0
- package/dist/cli/config/types.d.ts +50 -0
- package/dist/cli/config/types.js +33 -0
- package/dist/cli/deposit.d.ts +2 -0
- package/dist/cli/deposit.js +58 -0
- package/dist/cli/helpers.d.ts +12 -0
- package/dist/cli/helpers.js +53 -0
- package/dist/cli/note.d.ts +2 -0
- package/dist/cli/note.js +50 -0
- package/dist/cli/relayer.d.ts +2 -0
- package/dist/cli/relayer.js +60 -0
- package/dist/cli/substream.d.ts +2 -0
- package/dist/cli/substream.js +35 -0
- package/dist/cli/withdraw.d.ts +2 -0
- package/dist/cli/withdraw.js +23 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +30 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.js +77 -0
- package/dist/idl/voidify/idl.d.ts +1313 -0
- package/dist/idl/voidify/idl.js +1 -0
- package/dist/idl/voidify/idl.json +1307 -0
- package/dist/idl/voidify-staking/idl.d.ts +93 -0
- package/dist/idl/voidify-staking/idl.js +1 -0
- package/dist/idl/voidify-staking/idl.json +87 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/relayer/server/index.d.ts +6 -0
- package/dist/relayer/server/index.js +32 -0
- package/dist/relayer/server/server.d.ts +24 -0
- package/dist/relayer/server/server.js +158 -0
- package/dist/relayer/server/switchboard.d.ts +2 -0
- package/dist/relayer/server/switchboard.js +42 -0
- package/dist/relayer/types.d.ts +21 -0
- package/dist/relayer/types.js +1 -0
- package/dist/staking/commands.d.ts +3 -0
- package/dist/staking/commands.js +13 -0
- package/dist/staking/index.d.ts +2 -0
- package/dist/staking/index.js +2 -0
- package/dist/staking/program.d.ts +18 -0
- package/dist/staking/program.js +40 -0
- package/dist/substream/chain/events.d.ts +4 -0
- package/dist/substream/chain/events.js +50 -0
- package/dist/substream/chain/index.d.ts +24 -0
- package/dist/substream/chain/index.js +79 -0
- package/dist/substream/chain/registry.d.ts +44 -0
- package/dist/substream/chain/registry.js +28 -0
- package/dist/substream/chain/utils.d.ts +9 -0
- package/dist/substream/chain/utils.js +41 -0
- package/dist/substream/client.d.ts +27 -0
- package/dist/substream/client.js +28 -0
- package/dist/substream/database/indexeddb.d.ts +2 -0
- package/dist/substream/database/indexeddb.js +242 -0
- package/dist/substream/database/sqlite.d.ts +26 -0
- package/dist/substream/database/sqlite.js +275 -0
- package/dist/substream/modules/deposit.d.ts +14 -0
- package/dist/substream/modules/deposit.js +123 -0
- package/dist/substream/modules/index.d.ts +11 -0
- package/dist/substream/modules/index.js +7 -0
- package/dist/substream/modules/relayer.d.ts +10 -0
- package/dist/substream/modules/relayer.js +290 -0
- package/dist/substream/runtime.d.ts +38 -0
- package/dist/substream/runtime.js +163 -0
- package/dist/substream/server/event-listener.d.ts +18 -0
- package/dist/substream/server/event-listener.js +68 -0
- package/dist/substream/server/index.d.ts +3 -0
- package/dist/substream/server/index.js +30 -0
- package/dist/substream/server/server.d.ts +43 -0
- package/dist/substream/server/server.js +216 -0
- package/dist/substream/types.d.ts +94 -0
- package/dist/substream/types.js +1 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/errors.js +16 -0
- package/dist/types/events.d.ts +13 -0
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/amount.d.ts +4 -0
- package/dist/utils/amount.js +41 -0
- package/dist/utils/anchor-events.d.ts +13 -0
- package/dist/utils/anchor-events.js +28 -0
- package/dist/utils/bytes.d.ts +10 -0
- package/dist/utils/bytes.js +29 -0
- package/dist/utils/idl-seed.d.ts +17 -0
- package/dist/utils/idl-seed.js +15 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +19 -0
- package/dist/utils/note.d.ts +19 -0
- package/dist/utils/note.js +83 -0
- package/dist/utils/proof.d.ts +11 -0
- package/dist/utils/proof.js +91 -0
- package/dist/utils/tx.d.ts +11 -0
- package/dist/utils/tx.js +62 -0
- package/dist/voidify/deposit.d.ts +10 -0
- package/dist/voidify/deposit.js +40 -0
- package/dist/voidify/index.d.ts +4 -0
- package/dist/voidify/index.js +4 -0
- package/dist/voidify/program.d.ts +36 -0
- package/dist/voidify/program.js +87 -0
- package/dist/voidify/relayer/index.d.ts +1 -0
- package/dist/voidify/relayer/index.js +1 -0
- package/dist/voidify/relayer/list.d.ts +5 -0
- package/dist/voidify/relayer/list.js +16 -0
- package/dist/voidify/withdraw.d.ts +16 -0
- package/dist/voidify/withdraw.js +188 -0
- package/package.json +79 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { MerkleTree } from "fixed-merkle-tree";
|
|
2
|
+
import { buildBn128, utils } from "ffjavascript";
|
|
3
|
+
import { publicKeyToLoHi } from "../utils/bytes.js";
|
|
4
|
+
const { unstringifyBigInts } = utils;
|
|
5
|
+
const ZERO_ELEMENT = BigInt("0x28940deeacd1ca2831336874e87429db0e728a67a472b7ac8195c43c2fb13009").toString();
|
|
6
|
+
export async function generateMerkleProof(commitment, commitments) {
|
|
7
|
+
const { buildPoseidon } = await import("circomlibjs");
|
|
8
|
+
const poseidon = await buildPoseidon();
|
|
9
|
+
const leaves = commitments;
|
|
10
|
+
const tree = new MerkleTree(20, leaves, {
|
|
11
|
+
hashFunction: (a, b) => poseidon.F.toString(poseidon([BigInt(a), BigInt(b)])),
|
|
12
|
+
zeroElement: ZERO_ELEMENT,
|
|
13
|
+
});
|
|
14
|
+
const root = tree.root;
|
|
15
|
+
const leafIndex = leaves.findIndex((l) => l === commitment);
|
|
16
|
+
if (leafIndex === -1) {
|
|
17
|
+
throw new Error("Commitment not found in the on-chain tree. " +
|
|
18
|
+
"Ensure the deposit was confirmed before calling generateMerkleProof.");
|
|
19
|
+
}
|
|
20
|
+
const { pathElements, pathIndices } = tree.path(leafIndex);
|
|
21
|
+
return { pathElements, pathIndices, root };
|
|
22
|
+
}
|
|
23
|
+
export async function generateProof(nullifier, secret, amount, commitment, nullifierHash, recipient, relayer, fee, refund, commitments, wasmPath, zkeyPath) {
|
|
24
|
+
const { root, pathElements, pathIndices } = await generateMerkleProof(commitment, commitments);
|
|
25
|
+
const { lo: recipientLo, hi: recipientHi } = publicKeyToLoHi(recipient);
|
|
26
|
+
const { lo: relayerLo, hi: relayerHi } = publicKeyToLoHi(relayer);
|
|
27
|
+
const input = {
|
|
28
|
+
root,
|
|
29
|
+
nullifierHash: nullifierHash,
|
|
30
|
+
recipient_lo: recipientLo.toString(),
|
|
31
|
+
recipient_hi: recipientHi.toString(),
|
|
32
|
+
relayer_lo: relayerLo.toString(),
|
|
33
|
+
relayer_hi: relayerHi.toString(),
|
|
34
|
+
fee: fee.toString(),
|
|
35
|
+
refund: refund.toString(),
|
|
36
|
+
nullifier: nullifier,
|
|
37
|
+
secret: secret,
|
|
38
|
+
amount: amount,
|
|
39
|
+
pathElements,
|
|
40
|
+
pathIndices,
|
|
41
|
+
};
|
|
42
|
+
let snarkjs;
|
|
43
|
+
try {
|
|
44
|
+
snarkjs = await import("snarkjs");
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error("Failed to load snarkjs library. Please ensure it is installed.", {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { proof } = await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath);
|
|
53
|
+
return { root, proof };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new Error("Failed to generate zero-knowledge proof.", {
|
|
57
|
+
cause: error,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function g1UncompressedNegated(curve, p1Raw) {
|
|
62
|
+
const p1 = curve.G1.fromObject(p1Raw);
|
|
63
|
+
const negP1 = curve.G1.neg(p1);
|
|
64
|
+
const buff = new Uint8Array(64);
|
|
65
|
+
curve.G1.toRprUncompressed(buff, 0, negP1);
|
|
66
|
+
return Buffer.from(buff);
|
|
67
|
+
}
|
|
68
|
+
function g1Uncompressed(curve, p1Raw) {
|
|
69
|
+
let p1 = curve.G1.fromObject(p1Raw);
|
|
70
|
+
let buff = new Uint8Array(64);
|
|
71
|
+
curve.G1.toRprUncompressed(buff, 0, p1);
|
|
72
|
+
return Buffer.from(buff);
|
|
73
|
+
}
|
|
74
|
+
function g2Uncompressed(curve, p2Raw) {
|
|
75
|
+
let p2 = curve.G2.fromObject(p2Raw);
|
|
76
|
+
let buff = new Uint8Array(128);
|
|
77
|
+
curve.G2.toRprUncompressed(buff, 0, p2);
|
|
78
|
+
return Buffer.from(buff);
|
|
79
|
+
}
|
|
80
|
+
export const proofToBytes = async (proof) => {
|
|
81
|
+
let proofProc = unstringifyBigInts(proof);
|
|
82
|
+
let curve = await buildBn128();
|
|
83
|
+
const pi_a = g1UncompressedNegated(curve, proofProc.pi_a);
|
|
84
|
+
const pi_b = g2Uncompressed(curve, proofProc.pi_b);
|
|
85
|
+
const pi_c = g1Uncompressed(curve, proofProc.pi_c);
|
|
86
|
+
const allBytes = Buffer.concat([pi_a, pi_b, pi_c]);
|
|
87
|
+
if (allBytes.length !== 256) {
|
|
88
|
+
throw new Error(`Expected 256 bytes, but got ${allBytes.length}`);
|
|
89
|
+
}
|
|
90
|
+
return Array.from(allBytes);
|
|
91
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TransactionInstruction, VersionedTransaction } from "@solana/web3.js";
|
|
2
|
+
import { Context } from "../context.js";
|
|
3
|
+
import { SignReturn } from "../types/index.js";
|
|
4
|
+
export declare function buildTx(ctx: Context, txOrIxs: TransactionInstruction[] | VersionedTransaction): Promise<{
|
|
5
|
+
tx: VersionedTransaction;
|
|
6
|
+
blockhash: string;
|
|
7
|
+
lastValidBlockHeight: number;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function signAndSend(ctx: Context, txOrIxs: TransactionInstruction[] | VersionedTransaction): Promise<string>;
|
|
10
|
+
export declare function signOrBuild<S extends boolean = true>(ctx: Context, ixs: TransactionInstruction[], sign?: S): Promise<SignReturn<S>>;
|
|
11
|
+
export declare function makeCommand<TArgs extends unknown[]>(ixFn: (ctx: Context, ...args: TArgs) => Promise<TransactionInstruction[]>): <S extends boolean = true>(ctx: Context, ...args: [...TArgs, sign?: S]) => Promise<SignReturn<S>>;
|
package/dist/utils/tx.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
|
|
2
|
+
import bs58 from "bs58";
|
|
3
|
+
export async function buildTx(ctx, txOrIxs) {
|
|
4
|
+
if (Array.isArray(txOrIxs)) {
|
|
5
|
+
if (!ctx.wallet) {
|
|
6
|
+
throw new Error("wallet is required to build a transaction from ixs");
|
|
7
|
+
}
|
|
8
|
+
const latest = await ctx.connection.getLatestBlockhash("confirmed");
|
|
9
|
+
const tx = new VersionedTransaction(new TransactionMessage({
|
|
10
|
+
payerKey: ctx.wallet.publicKey,
|
|
11
|
+
recentBlockhash: latest.blockhash,
|
|
12
|
+
instructions: txOrIxs,
|
|
13
|
+
}).compileToV0Message());
|
|
14
|
+
return {
|
|
15
|
+
tx,
|
|
16
|
+
blockhash: latest.blockhash,
|
|
17
|
+
lastValidBlockHeight: latest.lastValidBlockHeight,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const tx = txOrIxs;
|
|
21
|
+
return {
|
|
22
|
+
tx,
|
|
23
|
+
blockhash: tx.message.recentBlockhash,
|
|
24
|
+
lastValidBlockHeight: (await ctx.connection.getBlockHeight("confirmed")) + 150,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function signAndSend(ctx, txOrIxs) {
|
|
28
|
+
if (!ctx.wallet) {
|
|
29
|
+
throw new Error("wallet is required to sign transactions");
|
|
30
|
+
}
|
|
31
|
+
const { tx, blockhash, lastValidBlockHeight } = await buildTx(ctx, txOrIxs);
|
|
32
|
+
const signedTx = await ctx.wallet.signTransaction(tx);
|
|
33
|
+
const signature = bs58.encode(signedTx.signatures[0]);
|
|
34
|
+
try {
|
|
35
|
+
await ctx.connection.sendTransaction(signedTx, {
|
|
36
|
+
preflightCommitment: "processed",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
if (!msg.toLowerCase().includes("already been processed")) {
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await ctx.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");
|
|
46
|
+
return signature;
|
|
47
|
+
}
|
|
48
|
+
export async function signOrBuild(ctx, ixs, sign = true) {
|
|
49
|
+
if (sign)
|
|
50
|
+
return (await signAndSend(ctx, ixs));
|
|
51
|
+
return (await buildTx(ctx, ixs)).tx;
|
|
52
|
+
}
|
|
53
|
+
export function makeCommand(ixFn) {
|
|
54
|
+
return async (ctx, ...rest) => {
|
|
55
|
+
const argCount = ixFn.length - 1;
|
|
56
|
+
const hasSign = rest.length > argCount;
|
|
57
|
+
const args = (hasSign ? rest.slice(0, argCount) : rest);
|
|
58
|
+
const sign = (hasSign ? rest[argCount] : true);
|
|
59
|
+
const ixs = await ixFn(ctx, ...args);
|
|
60
|
+
return signOrBuild(ctx, ixs, sign);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import { Context } from "../context.js";
|
|
3
|
+
import type { DepositRecord } from "../substream/types.js";
|
|
4
|
+
export declare function depositIx(ctx: Context, commitment: string, denomination: bigint): Promise<TransactionInstruction[]>;
|
|
5
|
+
export declare const deposit: <S extends boolean = true>(ctx: Context, commitment: string, denomination: bigint, sign?: S | undefined) => Promise<import("../index.js").SignReturn<S>>;
|
|
6
|
+
export declare function listDeposits(ctx: Context, denomination: bigint, options?: {
|
|
7
|
+
offset?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
output?: string;
|
|
10
|
+
}): Promise<DepositRecord[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { bigIntToBytes } from "../utils/bytes.js";
|
|
3
|
+
import { makeCommand } from "../utils/tx.js";
|
|
4
|
+
import { SubstreamCliClient } from "../substream/client.js";
|
|
5
|
+
import { VoidifyProgram } from "./program.js";
|
|
6
|
+
export async function depositIx(ctx, commitment, denomination) {
|
|
7
|
+
const commitmentBytes = bigIntToBytes(BigInt(commitment));
|
|
8
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
9
|
+
const poolPDA = voidifyProgram.pool(denomination);
|
|
10
|
+
const treasuryPDA = voidifyProgram.treasury();
|
|
11
|
+
const commitmentPDA = voidifyProgram.commitment(commitmentBytes);
|
|
12
|
+
const ix = await voidifyProgram.program.methods
|
|
13
|
+
.deposit(Array.from(commitmentBytes))
|
|
14
|
+
.accountsPartial({
|
|
15
|
+
sender: ctx.publicKey,
|
|
16
|
+
pool: poolPDA,
|
|
17
|
+
poolTreasury: treasuryPDA,
|
|
18
|
+
commitmentAccount: commitmentPDA,
|
|
19
|
+
systemProgram: PublicKey.default,
|
|
20
|
+
})
|
|
21
|
+
.instruction();
|
|
22
|
+
return [ix];
|
|
23
|
+
}
|
|
24
|
+
export const deposit = makeCommand(depositIx);
|
|
25
|
+
export async function listDeposits(ctx, denomination, options) {
|
|
26
|
+
const client = new SubstreamCliClient(ctx);
|
|
27
|
+
await client.init();
|
|
28
|
+
const depositModule = client.module("deposit");
|
|
29
|
+
await depositModule.sync(denomination);
|
|
30
|
+
const deposits = await depositModule.list(denomination, {
|
|
31
|
+
offset: options?.offset,
|
|
32
|
+
limit: options?.limit,
|
|
33
|
+
});
|
|
34
|
+
if (options?.output) {
|
|
35
|
+
const fs = await import("fs/promises");
|
|
36
|
+
const data = JSON.stringify(deposits, (_, v) => (typeof v === "bigint" ? v.toString() : v), 2);
|
|
37
|
+
await fs.writeFile(options.output, data, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
return deposits;
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Program } from "@coral-xyz/anchor";
|
|
2
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
3
|
+
import type { Voidify } from "../idl/voidify/idl.js";
|
|
4
|
+
export declare class VoidifyProgram {
|
|
5
|
+
private connection;
|
|
6
|
+
private _program;
|
|
7
|
+
private _programId;
|
|
8
|
+
static readonly SEEDS: {
|
|
9
|
+
readonly CORE_CONFIG: Buffer<ArrayBufferLike>;
|
|
10
|
+
readonly STAKE_CONFIG: Buffer<ArrayBufferLike>;
|
|
11
|
+
readonly TREASURY_CONFIG: Buffer<ArrayBufferLike>;
|
|
12
|
+
readonly ORACLE_CONFIG: Buffer<ArrayBufferLike>;
|
|
13
|
+
readonly POOL: Buffer<ArrayBufferLike>;
|
|
14
|
+
readonly TREASURY: Buffer<ArrayBufferLike>;
|
|
15
|
+
readonly COMMITMENT: Buffer<ArrayBufferLike>;
|
|
16
|
+
readonly NULLIFIER: Buffer<ArrayBufferLike>;
|
|
17
|
+
readonly RELAYER_CONFIG: Buffer<ArrayBufferLike>;
|
|
18
|
+
readonly RELAYER_EVENT_COUNTER: Buffer<ArrayBufferLike>;
|
|
19
|
+
};
|
|
20
|
+
static readonly BPF_LOADER_UPGRADEABLE_PROGRAM_ID: PublicKey;
|
|
21
|
+
constructor(connection: Connection, programId: PublicKey);
|
|
22
|
+
get program(): Program<Voidify>;
|
|
23
|
+
get programId(): PublicKey;
|
|
24
|
+
get rpcConnection(): Connection;
|
|
25
|
+
coreConfig(): PublicKey;
|
|
26
|
+
stakeConfig(): PublicKey;
|
|
27
|
+
treasuryConfig(): PublicKey;
|
|
28
|
+
oracleConfig(): PublicKey;
|
|
29
|
+
pool(denomination: number | bigint): PublicKey;
|
|
30
|
+
treasury(): PublicKey;
|
|
31
|
+
commitment(commitment: Uint8Array): PublicKey;
|
|
32
|
+
nullifier(nullifierHash: Uint8Array): PublicKey;
|
|
33
|
+
relayerConfig(relayerPubkey: PublicKey): PublicKey;
|
|
34
|
+
relayerEventCounter(): PublicKey;
|
|
35
|
+
programData(): PublicKey;
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Program } from "@coral-xyz/anchor";
|
|
2
|
+
import { PublicKey } from "@solana/web3.js";
|
|
3
|
+
import idl from "../idl/voidify/idl.json" with { type: "json" };
|
|
4
|
+
import { getConstSeed } from "../utils/idl-seed.js";
|
|
5
|
+
export class VoidifyProgram {
|
|
6
|
+
connection;
|
|
7
|
+
_program = null;
|
|
8
|
+
_programId;
|
|
9
|
+
static SEEDS = {
|
|
10
|
+
CORE_CONFIG: getConstSeed(idl, "core_config"),
|
|
11
|
+
STAKE_CONFIG: getConstSeed(idl, "stake_config"),
|
|
12
|
+
TREASURY_CONFIG: getConstSeed(idl, "treasury_config"),
|
|
13
|
+
ORACLE_CONFIG: getConstSeed(idl, "oracle_config"),
|
|
14
|
+
POOL: getConstSeed(idl, "pool"),
|
|
15
|
+
TREASURY: getConstSeed(idl, "treasury"),
|
|
16
|
+
COMMITMENT: getConstSeed(idl, "commitment"),
|
|
17
|
+
NULLIFIER: getConstSeed(idl, "nullifier"),
|
|
18
|
+
RELAYER_CONFIG: getConstSeed(idl, "relayer_config"),
|
|
19
|
+
RELAYER_EVENT_COUNTER: getConstSeed(idl, "relayer_event_counter"),
|
|
20
|
+
};
|
|
21
|
+
static BPF_LOADER_UPGRADEABLE_PROGRAM_ID = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111");
|
|
22
|
+
constructor(connection, programId) {
|
|
23
|
+
this.connection = connection;
|
|
24
|
+
this._programId = programId;
|
|
25
|
+
}
|
|
26
|
+
get program() {
|
|
27
|
+
if (!this._program) {
|
|
28
|
+
idl["address"] = this._programId.toBase58();
|
|
29
|
+
this._program = new Program(idl, {
|
|
30
|
+
connection: this.connection,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return this._program;
|
|
34
|
+
}
|
|
35
|
+
get programId() {
|
|
36
|
+
return this._programId;
|
|
37
|
+
}
|
|
38
|
+
get rpcConnection() {
|
|
39
|
+
return this.connection;
|
|
40
|
+
}
|
|
41
|
+
coreConfig() {
|
|
42
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.CORE_CONFIG], this._programId);
|
|
43
|
+
return pda;
|
|
44
|
+
}
|
|
45
|
+
stakeConfig() {
|
|
46
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.STAKE_CONFIG], this._programId);
|
|
47
|
+
return pda;
|
|
48
|
+
}
|
|
49
|
+
treasuryConfig() {
|
|
50
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.TREASURY_CONFIG], this._programId);
|
|
51
|
+
return pda;
|
|
52
|
+
}
|
|
53
|
+
oracleConfig() {
|
|
54
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.ORACLE_CONFIG], this._programId);
|
|
55
|
+
return pda;
|
|
56
|
+
}
|
|
57
|
+
pool(denomination) {
|
|
58
|
+
const denominationBuffer = Buffer.alloc(8);
|
|
59
|
+
denominationBuffer.writeBigUInt64BE(BigInt(denomination));
|
|
60
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.POOL, denominationBuffer], this._programId);
|
|
61
|
+
return pda;
|
|
62
|
+
}
|
|
63
|
+
treasury() {
|
|
64
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.TREASURY], this._programId);
|
|
65
|
+
return pda;
|
|
66
|
+
}
|
|
67
|
+
commitment(commitment) {
|
|
68
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.COMMITMENT, commitment], this._programId);
|
|
69
|
+
return pda;
|
|
70
|
+
}
|
|
71
|
+
nullifier(nullifierHash) {
|
|
72
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.NULLIFIER, nullifierHash], this._programId);
|
|
73
|
+
return pda;
|
|
74
|
+
}
|
|
75
|
+
relayerConfig(relayerPubkey) {
|
|
76
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.RELAYER_CONFIG, relayerPubkey.toBuffer()], this._programId);
|
|
77
|
+
return pda;
|
|
78
|
+
}
|
|
79
|
+
relayerEventCounter() {
|
|
80
|
+
const [pda] = PublicKey.findProgramAddressSync([VoidifyProgram.SEEDS.RELAYER_EVENT_COUNTER], this._programId);
|
|
81
|
+
return pda;
|
|
82
|
+
}
|
|
83
|
+
programData() {
|
|
84
|
+
const [pda] = PublicKey.findProgramAddressSync([this._programId.toBuffer()], VoidifyProgram.BPF_LOADER_UPGRADEABLE_PROGRAM_ID);
|
|
85
|
+
return pda;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./list.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./list.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SubstreamCliClient } from "../../substream/client.js";
|
|
2
|
+
export async function listRelayers(ctx, pubkey, options) {
|
|
3
|
+
const client = new SubstreamCliClient(ctx);
|
|
4
|
+
await client.init();
|
|
5
|
+
const relayerModule = client.module("relayer");
|
|
6
|
+
await relayerModule.sync();
|
|
7
|
+
const result = pubkey
|
|
8
|
+
? await relayerModule.get(pubkey)
|
|
9
|
+
: await relayerModule.list();
|
|
10
|
+
if (options?.output) {
|
|
11
|
+
const fs = await import("fs/promises");
|
|
12
|
+
const data = JSON.stringify(result, null, 2);
|
|
13
|
+
await fs.writeFile(options.output, data, "utf-8");
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import { Context } from "../context.js";
|
|
3
|
+
import { Note } from "../utils/note.js";
|
|
4
|
+
export declare function directWithdrawIx(ctx: Context, proof: Uint8Array, root: Uint8Array, nullifierHash: Uint8Array, recipient: string, fee: bigint, treasury: bigint, denomination: bigint): Promise<TransactionInstruction[]>;
|
|
5
|
+
export declare function withdrawIx(ctx: Context, proof: Uint8Array, root: Uint8Array, nullifierHash: Uint8Array, recipient: string, relayer: string, fee: bigint, treasury: bigint, switchboardQuote: PublicKey, denomination: bigint): Promise<TransactionInstruction[]>;
|
|
6
|
+
export interface WithdrawArtifact {
|
|
7
|
+
withdrawData: string;
|
|
8
|
+
relayerUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateNote(ctx: Context, note_str: string): Promise<{
|
|
11
|
+
note: Note;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function prepareWithdraw(ctx: Context, note_str: string, recipient: string, relayer: string): Promise<WithdrawArtifact>;
|
|
14
|
+
export declare function submitWithdrawToRelayer(artifact: WithdrawArtifact): Promise<string>;
|
|
15
|
+
export declare function withdraw(ctx: Context, note_str: string, recipient: string, relayer: string, _dryRun: boolean): Promise<string>;
|
|
16
|
+
export declare function directWithdraw(ctx: Context, note_str: string, recipient: string): Promise<string>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ComputeBudgetProgram, PublicKey, } from "@solana/web3.js";
|
|
2
|
+
import { bigIntToBytes } from "../utils/bytes.js";
|
|
3
|
+
import { Note } from "../utils/note.js";
|
|
4
|
+
import { generateProof, proofToBytes } from "../utils/proof.js";
|
|
5
|
+
import { toBN } from "../utils/amount.js";
|
|
6
|
+
import { signAndSend } from "../utils/tx.js";
|
|
7
|
+
import { SubstreamCliClient } from "../substream/client.js";
|
|
8
|
+
import { VoidifyProgram } from "./program.js";
|
|
9
|
+
const WITHDRAW_COMPUTE_UNIT_LIMIT = 600_000;
|
|
10
|
+
function withdrawComputeBudgetIx() {
|
|
11
|
+
return ComputeBudgetProgram.setComputeUnitLimit({
|
|
12
|
+
units: WITHDRAW_COMPUTE_UNIT_LIMIT,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function directWithdrawIx(ctx, proof, root, nullifierHash, recipient, fee, treasury, denomination) {
|
|
16
|
+
const recipientPubkey = new PublicKey(recipient);
|
|
17
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
18
|
+
const treasuryConfig = await voidifyProgram.program.account.treasuryConfig.fetch(voidifyProgram.treasuryConfig());
|
|
19
|
+
const ix = await voidifyProgram.program.methods
|
|
20
|
+
.directWithdraw(Array.from(proof), Array.from(root), Array.from(nullifierHash), toBN(fee), toBN(treasury))
|
|
21
|
+
.accountsPartial({
|
|
22
|
+
sender: ctx.publicKey,
|
|
23
|
+
recipient: recipientPubkey,
|
|
24
|
+
pool: voidifyProgram.pool(denomination),
|
|
25
|
+
treasurySolDestination: treasuryConfig.treasurySolAddress,
|
|
26
|
+
})
|
|
27
|
+
.instruction();
|
|
28
|
+
return [withdrawComputeBudgetIx(), ix];
|
|
29
|
+
}
|
|
30
|
+
export async function withdrawIx(ctx, proof, root, nullifierHash, recipient, relayer, fee, treasury, switchboardQuote, denomination) {
|
|
31
|
+
const recipientPubkey = new PublicKey(recipient);
|
|
32
|
+
const relayerPubkey = new PublicKey(relayer);
|
|
33
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
34
|
+
const [treasuryConfig, stakeConfig] = await Promise.all([
|
|
35
|
+
voidifyProgram.program.account.treasuryConfig.fetch(voidifyProgram.treasuryConfig()),
|
|
36
|
+
voidifyProgram.program.account.stakeConfig.fetch(voidifyProgram.stakeConfig()),
|
|
37
|
+
]);
|
|
38
|
+
const ix = await voidifyProgram.program.methods
|
|
39
|
+
.withdraw(Array.from(proof), Array.from(root), Array.from(nullifierHash), toBN(fee), toBN(treasury))
|
|
40
|
+
.accountsPartial({
|
|
41
|
+
relayer: relayerPubkey,
|
|
42
|
+
recipient: recipientPubkey,
|
|
43
|
+
pool: voidifyProgram.pool(denomination),
|
|
44
|
+
switchboardQuote,
|
|
45
|
+
stakeTokenMint: stakeConfig.stakeTokenMint,
|
|
46
|
+
stakingRewardVault: treasuryConfig.stakingRewardVault,
|
|
47
|
+
treasuryTokenAccount: treasuryConfig.treasuryTokenAccount,
|
|
48
|
+
})
|
|
49
|
+
.instruction();
|
|
50
|
+
return [withdrawComputeBudgetIx(), ix];
|
|
51
|
+
}
|
|
52
|
+
export async function validateNote(ctx, note_str) {
|
|
53
|
+
const note = await Note.deserialize(note_str);
|
|
54
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
55
|
+
const nullifierBytes = bigIntToBytes(BigInt(note.nullifierHash));
|
|
56
|
+
const nullifierPda = voidifyProgram.nullifier(nullifierBytes);
|
|
57
|
+
const nullifierAccount = await ctx.connection.getAccountInfo(nullifierPda);
|
|
58
|
+
if (nullifierAccount !== null) {
|
|
59
|
+
throw new Error("Note already used: this note has been withdrawn. Each deposit note can be used only once.");
|
|
60
|
+
}
|
|
61
|
+
const client = new SubstreamCliClient(ctx);
|
|
62
|
+
await client.init();
|
|
63
|
+
const depositModule = client.module("deposit");
|
|
64
|
+
await depositModule.sync(note.amountRaw);
|
|
65
|
+
const deposits = await depositModule.list(note.amountRaw);
|
|
66
|
+
const commitmentInPool = deposits.some((d) => d.commitment === note.commitment);
|
|
67
|
+
if (!commitmentInPool) {
|
|
68
|
+
throw new Error("Invalid note: deposit not found in the pool. Verify the note is correct and the deposit transaction has been confirmed.");
|
|
69
|
+
}
|
|
70
|
+
return { note };
|
|
71
|
+
}
|
|
72
|
+
export async function prepareWithdraw(ctx, note_str, recipient, relayer) {
|
|
73
|
+
const note = await Note.deserialize(note_str);
|
|
74
|
+
const recipientPubkey = new PublicKey(recipient);
|
|
75
|
+
const relayerPubkey = new PublicKey(relayer);
|
|
76
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
77
|
+
const nullifierBytes = bigIntToBytes(BigInt(note.nullifierHash));
|
|
78
|
+
const nullifierPda = voidifyProgram.nullifier(nullifierBytes);
|
|
79
|
+
const nullifierAccount = await ctx.connection.getAccountInfo(nullifierPda);
|
|
80
|
+
if (nullifierAccount !== null) {
|
|
81
|
+
throw new Error("Note already used: this note has been withdrawn. Each deposit note can be used only once.");
|
|
82
|
+
}
|
|
83
|
+
const client = new SubstreamCliClient(ctx);
|
|
84
|
+
await client.init();
|
|
85
|
+
const depositModule = client.module("deposit");
|
|
86
|
+
await depositModule.sync(note.amountRaw);
|
|
87
|
+
const deposits = await depositModule.list(note.amountRaw);
|
|
88
|
+
const commitments = [...deposits]
|
|
89
|
+
.sort((a, b) => a.index - b.index)
|
|
90
|
+
.map((d) => d.commitment);
|
|
91
|
+
if (commitments.length === 0) {
|
|
92
|
+
throw new Error("Failed to get commitments.");
|
|
93
|
+
}
|
|
94
|
+
if (!commitments.includes(note.commitment)) {
|
|
95
|
+
throw new Error("Invalid note: deposit not found in the pool. Verify the note is correct and the deposit transaction has been confirmed.");
|
|
96
|
+
}
|
|
97
|
+
const relayerModule = client.module("relayer");
|
|
98
|
+
await relayerModule.sync();
|
|
99
|
+
const relayerInfo = await relayerModule.get(relayer);
|
|
100
|
+
if (!relayerInfo) {
|
|
101
|
+
throw new Error("Failed to get relayer info");
|
|
102
|
+
}
|
|
103
|
+
const treasuryConfig = await voidifyProgram.program.account.treasuryConfig.fetch(voidifyProgram.treasuryConfig());
|
|
104
|
+
const fee = (note.amountRaw * BigInt(relayerInfo.feeBps)) / BigInt(10000);
|
|
105
|
+
const treasury = (note.amountRaw * BigInt(treasuryConfig.treasuryBps)) / BigInt(10000);
|
|
106
|
+
const { root, proof } = await generateProof(note.nullifier, note.secret, note.amountRaw.toString(), note.commitment, note.nullifierHash, recipientPubkey, relayerPubkey, fee, treasury, commitments, ctx.wasmPath, ctx.zkeyPath);
|
|
107
|
+
const withdrawData = JSON.stringify({
|
|
108
|
+
proof: await proofToBytes(proof),
|
|
109
|
+
root: Array.from(bigIntToBytes(BigInt(root))),
|
|
110
|
+
nullifierHash: Array.from(bigIntToBytes(BigInt(note.nullifierHash))),
|
|
111
|
+
recipient,
|
|
112
|
+
amount: note.amountRaw.toString(),
|
|
113
|
+
fee: fee.toString(),
|
|
114
|
+
treasury: treasury.toString(),
|
|
115
|
+
});
|
|
116
|
+
return { withdrawData, relayerUrl: relayerInfo.url };
|
|
117
|
+
}
|
|
118
|
+
export async function submitWithdrawToRelayer(artifact) {
|
|
119
|
+
const response = await fetch(artifact.relayerUrl + "/api/relay/withdraw", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
body: artifact.withdrawData,
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const errorBody = await response.text().catch(() => "<unreadable body>");
|
|
126
|
+
let formatted = errorBody;
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(errorBody);
|
|
129
|
+
formatted =
|
|
130
|
+
typeof parsed?.error === "string"
|
|
131
|
+
? parsed.error
|
|
132
|
+
: JSON.stringify(parsed, null, 2);
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
throw new Error(`Failed to send withdraw data: ${response.status} ${response.statusText}\n${formatted}`);
|
|
136
|
+
}
|
|
137
|
+
const responseText = await response.text();
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
result = JSON.parse(responseText);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error(`Relayer returned non-JSON response: ${responseText.slice(0, 200)}`);
|
|
144
|
+
}
|
|
145
|
+
if (!result?.success || !result?.signature) {
|
|
146
|
+
throw new Error(result?.error || "Relayer returned unsuccessful response");
|
|
147
|
+
}
|
|
148
|
+
return result.signature;
|
|
149
|
+
}
|
|
150
|
+
export async function withdraw(ctx, note_str, recipient, relayer, _dryRun) {
|
|
151
|
+
const artifact = await prepareWithdraw(ctx, note_str, recipient, relayer);
|
|
152
|
+
return submitWithdrawToRelayer(artifact);
|
|
153
|
+
}
|
|
154
|
+
export async function directWithdraw(ctx, note_str, recipient) {
|
|
155
|
+
const note = await Note.deserialize(note_str);
|
|
156
|
+
const recipientPubkey = new PublicKey(recipient);
|
|
157
|
+
const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
158
|
+
const nullifierBytes = bigIntToBytes(BigInt(note.nullifierHash));
|
|
159
|
+
const nullifierPda = voidifyProgram.nullifier(nullifierBytes);
|
|
160
|
+
const nullifierAccount = await ctx.connection.getAccountInfo(nullifierPda);
|
|
161
|
+
if (nullifierAccount !== null) {
|
|
162
|
+
throw new Error("Note already used: this note has been withdrawn. Each deposit note can be used only once.");
|
|
163
|
+
}
|
|
164
|
+
const client = new SubstreamCliClient(ctx);
|
|
165
|
+
await client.init();
|
|
166
|
+
const depositModule = client.module("deposit");
|
|
167
|
+
await depositModule.sync(note.amountRaw);
|
|
168
|
+
const deposits = await depositModule.list(note.amountRaw);
|
|
169
|
+
const commitments = [...deposits]
|
|
170
|
+
.sort((a, b) => a.index - b.index)
|
|
171
|
+
.map((d) => d.commitment);
|
|
172
|
+
if (commitments.length === 0) {
|
|
173
|
+
throw new Error("Failed to get commitments.");
|
|
174
|
+
}
|
|
175
|
+
if (!commitments.includes(note.commitment)) {
|
|
176
|
+
throw new Error("Invalid note: deposit not found in the pool. Verify the note is correct and the deposit transaction has been confirmed.");
|
|
177
|
+
}
|
|
178
|
+
const treasuryConfig = await voidifyProgram.program.account.treasuryConfig.fetch(voidifyProgram.treasuryConfig());
|
|
179
|
+
const fee = 0n;
|
|
180
|
+
const treasury = (note.amountRaw * BigInt(treasuryConfig.directWithdrawBps)) / 10000n;
|
|
181
|
+
const senderAsRelayer = ctx.publicKey;
|
|
182
|
+
const { root, proof } = await generateProof(note.nullifier, note.secret, note.amountRaw.toString(), note.commitment, note.nullifierHash, recipientPubkey, senderAsRelayer, fee, treasury, commitments, ctx.wasmPath, ctx.zkeyPath);
|
|
183
|
+
const proofBytes = new Uint8Array(await proofToBytes(proof));
|
|
184
|
+
const rootBytes = bigIntToBytes(BigInt(root));
|
|
185
|
+
const nullifierHashBytes = bigIntToBytes(BigInt(note.nullifierHash));
|
|
186
|
+
const ixs = await directWithdrawIx(ctx, proofBytes, rootBytes, nullifierHashBytes, recipient, fee, treasury, note.amountRaw);
|
|
187
|
+
return signAndSend(ctx, ixs);
|
|
188
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voidifydao/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Voidify client SDK and CLI for interacting with the Anchor voidify program suite (browser + Node).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"voidify": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && tsc-alias",
|
|
25
|
+
"start": "npm run build && node dist/cli.js",
|
|
26
|
+
"dev": "tsx src/cli.ts",
|
|
27
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
+
"lint": "eslint src/**/*.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@coral-xyz/anchor": "^0.32.1",
|
|
32
|
+
"@solana/spl-token": "^0.4.14",
|
|
33
|
+
"@solana/web3.js": "^1.98.4",
|
|
34
|
+
"@switchboard-xyz/common": "^5.8.1",
|
|
35
|
+
"@switchboard-xyz/on-demand": "^3.10.1",
|
|
36
|
+
"better-sqlite3": "^12.9.0",
|
|
37
|
+
"bn.js": "^5.2.3",
|
|
38
|
+
"bs58": "^6.0.0",
|
|
39
|
+
"circomlibjs": "^0.1.7",
|
|
40
|
+
"commander": "^14.0.3",
|
|
41
|
+
"conf": "^15.1.0",
|
|
42
|
+
"cors": "^2.8.6",
|
|
43
|
+
"dexie": "^4.4.2",
|
|
44
|
+
"express": "^5.2.1",
|
|
45
|
+
"ffjavascript": "^0.3.1",
|
|
46
|
+
"fixed-merkle-tree": "^0.7.3",
|
|
47
|
+
"pino": "^10.3.1",
|
|
48
|
+
"pino-pretty": "^13.1.3",
|
|
49
|
+
"snarkjs": "^0.7.6",
|
|
50
|
+
"yaml": "^2.9.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
54
|
+
"@types/bn.js": "^5.2.0",
|
|
55
|
+
"@types/circomlibjs": "^0.1.6",
|
|
56
|
+
"@types/cors": "^2.8.19",
|
|
57
|
+
"@types/express": "^5.0.6",
|
|
58
|
+
"@types/node": "^20.11.0",
|
|
59
|
+
"@types/snarkjs": "^0.7.9",
|
|
60
|
+
"esbuild": "^0.28.0",
|
|
61
|
+
"prettier": "^3.8.3",
|
|
62
|
+
"ts-node": "^10.9.2",
|
|
63
|
+
"tsc-alias": "^1.8.17",
|
|
64
|
+
"tsx": "^4.21.0",
|
|
65
|
+
"typescript": "^6.0.3"
|
|
66
|
+
},
|
|
67
|
+
"keywords": [
|
|
68
|
+
"solana",
|
|
69
|
+
"anchor",
|
|
70
|
+
"voidify",
|
|
71
|
+
"cli"
|
|
72
|
+
],
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/VoidifyCommunity/voidify-sdk.git"
|
|
76
|
+
},
|
|
77
|
+
"author": "",
|
|
78
|
+
"license": "MIT"
|
|
79
|
+
}
|