Create project directory
mkdir solana-pay-pirates
cd solana-pay-pirates
Initialize npm
npm init -y
Create api directory with index file
mkdir api
touch api/index.js
Install vercel and ngrok locally
npm i -D vercel ngrok
Scaffold server
// api/index.js
/**
* @typedef {import('@vercel/node').VercelResponse} VercelResponse
* @typedef {import('@vercel/node').VercelRequest} VercelRequest
*
* @param {VercelRequest} request
* @param {VercelResponse} response
* @returns {Promise<VercelResponse>}
* */
export default async function handler(request, response) {
console.log('handling request', request.method);
return response.status(200).json({});
}
Start local dev server
npx vercel dev
Navigate to http://localhost:3000/api
In another terminal, start ngrok
npx ngrok http 3000
With ngrok running, you can now test your local server from the internet.
Navigate to the ngrok url in your browser and you should see the same response as before.
Create a QR code
The data should be: solana:<link>
Where <link> is your ngrok url
Scan the QR code via your mobile wallet -- what happens? what do your logs say?
Create a function to handle the GET request
/**
* @param {VercelResponse} response
*/
function handleGet(response) {
return response.status(200).json({
label: 'Chutulu Fire!',
icon: 'https://github.com/solana-developers/pirate-bootcamp/blob/main/assets/kraken-1.png?raw=true',
});
}
Update the handler to use the function
export default async function handler(request, response) {
console.log('handling request', request.method);
+ if (request.method === 'GET') {
- return response.status(200).json({});
+ return handleGet(response);
+ } else {
+ return response.status(405).json({ error: 'Method not allowed' });
+ }
}
Create a function to handle the POST request
/**
* @param {VercelRequest} request
* @param {VercelResponse} response
*/
async function handlePost(request, response) {
console.log('account', request.body .account);
return response.status(200).json({
transaction: 'TODO',
message: 'Chutulu Fire!',
});
}
Update the handler to use the function
export default async function handler(request, response) {
console.log('handling request', request.method);
if (request.method === 'GET') {
return handleGet(response);
- } else {
+ } else if (request.method === 'POST') {
+ return handlePost(request, response);
} else {
return response.status(405).json({ error: 'Method not allowed' });
}
}
Let's scaffold our handlePost
function
+ import { PublicKey } from '@solana/web3.js';
async function handlePost(request, response) {
- console.log('account', request.body .account);
+ const player = new PublicKey(request.body.account);
+
+ const chutuluIx = await createChutuluIx(player);
+
+ const transaction = await prepareTx(chutuluIx);
return response.status(200).json({
- transaction: 'TODO',
+ transaction,
message: 'Chutulu Fire!',
});
}
Install @solana/web3.js
npm i @solana/web3.js
Create createChutuluIx
function
/**
* @typedef {import('@solana/spl-token').Account} Account
*
* @param {PublicKey} player
* @returns {Promise<TransactionInstruction>}
*/
async function createChutuluIx(player) {
// get player's GOLD token account
// get accounts for chutuluIX
// return the instruction
}
Get the player's GOLD token account
async function createChutuluIx(player) {
// get player's GOLD token account
+ const playerTokenAccount = await getOrCreateAssociatedTokenAccount(
+ connection,
+ payer,
+ GOLD_TOKEN_MINT,
+ player,
+ );
// get accounts for chutuluIX
// return the instruction
}
Install new deps
npm i @solana/spl-token dotenv bs58
Update imports
- import { PublicKey } from '@solana/web3.js';
+ import { PublicKey, Keypair, Connection, TransactionInstruction } from '@solana/web3.js';
+ import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
+ import bs58 from 'bs58';
+ import dotenv from 'dotenv';
Define connection, payer and GOLD_TOKEN_MINT variables
+ dotenv.config();
+ const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
+ const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER));
+ const GOLD_TOKEN_MINT = new PublicKey('goLdQwNaZToyavwkbuPJzTt5XPNR3H7WQBGenWtzPH3');
🏁 At this point, your createChutuluIx
should look like the below:
import { PublicKey, Keypair, Connection, TransactionInstruction } from '@solana/web3.js';
import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
import dotenv from 'dotenv';
dotenv.config();
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER));
const GOLD_TOKEN_MINT = new PublicKey('goLdQwNaZToyavwkbuPJzTt5XPNR3H7WQBGenWtzPH3');
// ...
/**
* @typedef {import('@solana/spl-token').Account} Account
*
* @param {PublicKey} player
* @returns {Promise<TransactionInstruction>}
*/
async function createChutuluIx(player) {
// get player's GOLD token account
const playerTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
GOLD_TOKEN_MINT,
player,
);
// get accounts for chutuluIX
// return the instruction
}
Get all the accounts needed for the chutulu instruction
async function createChutuluIx(player) {
// get player's GOLD token account
const playerTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
GOLD_TOKEN_MINT,
player,
);
// get accounts for chutuluIX
+ // start: get program derived addresses
+ const [level] = PublicKey.findProgramAddressSync([Buffer.from('level')], SEVEN_SEAS_PROGRAM);
+
+ const [chestVault] = PublicKey.findProgramAddressSync(
+ [Buffer.from('chestVault')],
+ SEVEN_SEAS_PROGRAM,
+ );
+
+ const [gameActions] = PublicKey.findProgramAddressSync(
+ [Buffer.from('gameActions')],
+ SEVEN_SEAS_PROGRAM,
+ );
+
+ let [tokenAccountOwnerPda] = await PublicKey.findProgramAddressSync(
+ [Buffer.from('token_account_owner_pda', 'utf8')],
+ SEVEN_SEAS_PROGRAM,
+ );
+
+ let [tokenVault] = await PublicKey.findProgramAddressSync(
+ [Buffer.from('token_vault', 'utf8'), GOLD_TOKEN_MINT.toBuffer()],
+ SEVEN_SEAS_PROGRAM,
+ );
+ // end: get program derived addresses
+
// return the instruction
}
Create the chutulu instruction
async function createChutuluIx(player) {
// get player's GOLD token account
const playerTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
GOLD_TOKEN_MINT,
player,
);
// get accounts for chutuluIX
// start: get program derived addresses
const [level] = PublicKey.findProgramAddressSync([Buffer.from('level')], SEVEN_SEAS_PROGRAM);
const [chestVault] = PublicKey.findProgramAddressSync(
[Buffer.from('chestVault')],
SEVEN_SEAS_PROGRAM,
);
const [gameActions] = PublicKey.findProgramAddressSync(
[Buffer.from('gameActions')],
SEVEN_SEAS_PROGRAM,
);
let [tokenAccountOwnerPda] = await PublicKey.findProgramAddressSync(
[Buffer.from('token_account_owner_pda', 'utf8')],
SEVEN_SEAS_PROGRAM,
);
let [tokenVault] = await PublicKey.findProgramAddressSync(
[Buffer.from('token_vault', 'utf8'), GOLD_TOKEN_MINT.toBuffer()],
SEVEN_SEAS_PROGRAM,
);
// end: get program derived addresses
// return the instruction
+ return new TransactionInstruction({
+ programId: SEVEN_SEAS_PROGRAM,
+ keys: [
+ {
+ pubkey: chestVault,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: level,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: gameActions,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: player,
+ isWritable: true,
+ isSigner: true,
+ },
+ {
+ pubkey: SystemProgram.programId,
+ isWritable: false,
+ isSigner: false,
+ },
+ {
+ pubkey: player,
+ isWritable: false,
+ isSigner: false,
+ },
+ {
+ pubkey: playerTokenAccount.address,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: tokenVault,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: tokenAccountOwnerPda,
+ isWritable: true,
+ isSigner: false,
+ },
+ {
+ pubkey: GOLD_TOKEN_MINT,
+ isWritable: false,
+ isSigner: false,
+ },
+ {
+ pubkey: TOKEN_PROGRAM_ID,
+ isWritable: false,
+ isSigner: false,
+ },
+ {
+ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+ isWritable: false,
+ isSigner: false,
+ },
+ ],
+ data: Buffer.from(new Uint8Array([84, 206, 8, 255, 98, 163, 218, 19, 1])),
+ });
}
Add missing imports
- import { PublicKey, Keypair, Connection, TransactionInstruction } from '@solana/web3.js';
+ import { PublicKey, Keypair, Connection, Transaction, TransactionInstruction, SystemProgram } from '@solana/web3.js';
- import { getOrCreateAssociatedTokenAccount, } from '@solana/spl-token';
+ import { getOrCreateAssociatedTokenAccount, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import dotenv from 'dotenv';
Add missing constant
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER));
const GOLD_TOKEN_MINT = new PublicKey('goLdQwNaZToyavwkbuPJzTt5XPNR3H7WQBGenWtzPH3');
+ const SEVEN_SEAS_PROGRAM = new PublicKey('2a4NcnkF5zf14JQXHAv39AsRf7jMFj13wKmTL6ZcDQNd');
🙏🏾 We're almost there!
Create the prepareTx
function
/**
* @param {TransactionInstruction} ix
*/
async function prepareTx(ix) {
let tx = new Transaction().add(ix);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = payer.publicKey;
}
Partially sign the transaction -- why?
async function prepareTx(ix) {
let tx = new Transaction().add(ix);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = payer.publicKey;
+ tx.partialSign(payer);
}
Do a dance to serialize the transaction and return it
async function prepareTx(ix) {
let tx = new Transaction().add(ix);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = payer.publicKey;
tx.partialSign(payer);
+ tx = Transaction.from(
+ tx.serialize({
+ verifySignatures: false,
+ requireAllSignatures: false,
+ }),
+ );
+
+ const serializedTx = tx.serialize({
+ verifySignatures: false,
+ requireAllSignatures: false,
+ });
+ // end: dance
+
+ return serializedTx.toString('base64');
}
🌟 We're all done, scan your QR code and see Chutulu in action!