Skip to content

Commit f04f7db

Browse files
add idempotent example
1 parent 0523803 commit f04f7db

File tree

5 files changed

+87
-18
lines changed

5 files changed

+87
-18
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"airdrop": "ts-node src/scripts/airdrop/airdrop.ts",
2222
"transfer": "ts-node src/scripts/transfer.ts",
2323
"compress": "ts-node src/scripts/compress.ts",
24+
2425
"test": "npm run lamports && npm run token && npm run connection && npm run batch-compress && npm run mint-spl && npm run mint-spl-22 && npm run mint-spl-22-manual"
2526
},
2627
"repository": {

src/scripts/airdrop/airdrop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const recipients = [
6565
);
6666
console.log(`ATA: ${ata.address.toBase58()}`);
6767

68-
// /// Mint SPL tokens to the sender
68+
/// Mint SPL tokens to the sender
6969
const mintToTxId = await mintTo(
7070
connection,
7171
PAYER,

src/scripts/compress.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
calculateComputeUnitPrice,
1212
createRpc,
1313
dedupeSigner,
14-
pickRandomTreeAndQueue,
1514
Rpc,
1615
selectStateTreeInfo,
1716
sendAndConfirmTx,
1817
} from "@lightprotocol/stateless.js";
1918
import * as splToken from "@solana/spl-token";
2019
import dotenv from "dotenv";
2120
import bs58 from "bs58";
21+
import { createIdempotentAirdropInstruction } from "./idempotent";
2222
dotenv.config();
2323

2424
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
@@ -34,9 +34,10 @@ const PAYER_KEYPAIR = Keypair.fromSecretKey(
3434
const payer = PAYER_KEYPAIR;
3535

3636
const amount = bn(333); // each recipient will receive 111 tokens
37-
const recipients = ["GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7"].map(
38-
(address) => new PublicKey(address)
39-
);
37+
const recipients = [
38+
"GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7",
39+
"GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7",
40+
].map((address) => new PublicKey(address));
4041
const activeStateTrees = await connection.getStateTreeInfos();
4142

4243
/// Pick a new tree for each transaction!
@@ -54,21 +55,20 @@ const PAYER_KEYPAIR = Keypair.fromSecretKey(
5455
// Airdrop to example recipients addresses
5556
// 1 recipient = 120_000 CU
5657
// 5 recipients = 170_000 CU
57-
58+
// with idempotent cPDA = +250_000 CU
5859
const instructions: web3.TransactionInstruction[] = [];
59-
6060
instructions.push(
61-
web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 120_000 }),
61+
web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 370_000 }),
6262
web3.ComputeBudgetProgram.setComputeUnitPrice({
63-
// ideally replace this with a dynamic priority_fee based on network conditions.
64-
microLamports: calculateComputeUnitPrice(20_000, 120_000),
63+
// Replace this with a dynamic priority_fee to land during high network load.
64+
microLamports: calculateComputeUnitPrice(20_000, 370_000),
6565
})
6666
);
6767

6868
const compressInstruction = await CompressedTokenProgram.compress({
6969
payer: payer.publicKey,
7070
owner: payer.publicKey,
71-
source: sourceTokenAccount.address, // here, the owner of this account is also the payer.
71+
source: sourceTokenAccount.address,
7272
toAddress: recipients,
7373
amount: recipients.map(() => amount),
7474
mint: mintAddress,
@@ -79,13 +79,23 @@ const PAYER_KEYPAIR = Keypair.fromSecretKey(
7979
});
8080
instructions.push(compressInstruction);
8181

82+
// Creates a cPDA for a given set of recipients. This lets you retry txns without handling spends client-side.
83+
// The whole txn will fail if the same set of seeds (with the same order) is used a second time.
84+
instructions.push(
85+
await createIdempotentAirdropInstruction(
86+
connection,
87+
payer.publicKey,
88+
mintAddress,
89+
recipients,
90+
treeInfo
91+
)
92+
);
93+
8294
// Use zk-compression LUT for your network
8395
// https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
8496
const lookupTableAddress = new web3.PublicKey(
8597
"9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ" // mainnet
86-
// "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" // devnet
8798
);
88-
8999
// Get the lookup table account state
90100
const lookupTableAccount = (
91101
await connection.getAddressLookupTable(lookupTableAddress)
@@ -105,7 +115,6 @@ const PAYER_KEYPAIR = Keypair.fromSecretKey(
105115
[lookupTableAccount]
106116
);
107117

108-
// Uncomment to send the transaction.
109118
const txId = await sendAndConfirmTx(connection, tx);
110119
console.log(`txId: ${txId}`);
111120
} catch (e) {

src/scripts/idempotent.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2+
import {
3+
bn,
4+
Rpc,
5+
LightSystemProgram,
6+
getDefaultAddressTreeInfo,
7+
deriveAddressSeed,
8+
deriveAddress,
9+
TreeInfo,
10+
NewAddressParams,
11+
} from "@lightprotocol/stateless.js";
12+
13+
// This is a helper function to create a cPDA for a given set of recipients.
14+
// This lets you retry txns without handling spends client-side.
15+
// The whole txn will fail if the same set of seeds (with the same order) is used a second time.
16+
// Change the seeds to fit your use case.
17+
export async function createIdempotentAirdropInstruction(
18+
rpc: Rpc,
19+
payer: PublicKey,
20+
mint: PublicKey,
21+
recipients: PublicKey[],
22+
outputStateTreeInfo: TreeInfo
23+
): Promise<TransactionInstruction> {
24+
const { tree, queue } = getDefaultAddressTreeInfo();
25+
26+
const seeds = recipients
27+
.map((recipient) => recipient.toBytes())
28+
.concat(mint.toBytes())
29+
.concat([new Uint8Array([1])]); // you can pick a discriminator so as to avoid collision in between different drops.
30+
31+
const seed = deriveAddressSeed(seeds, LightSystemProgram.programId);
32+
const address = deriveAddress(seed, tree);
33+
34+
const proof = await rpc.getValidityProofV0(undefined, [
35+
{
36+
address: bn(address.toBytes()),
37+
tree,
38+
queue,
39+
},
40+
]);
41+
42+
const params: NewAddressParams = {
43+
seed: seed,
44+
addressMerkleTreeRootIndex: proof.rootIndices[0],
45+
addressMerkleTreePubkey: proof.treeInfos[0].tree,
46+
addressQueuePubkey: proof.treeInfos[0].queue,
47+
};
48+
49+
const ix = await LightSystemProgram.createAccount({
50+
payer,
51+
newAddressParams: params,
52+
newAddress: Array.from(address.toBytes()),
53+
recentValidityProof: proof.compressedProof,
54+
programId: LightSystemProgram.programId,
55+
outputStateTreeInfo,
56+
});
57+
58+
return ix;
59+
}

0 commit comments

Comments
 (0)