From 7e31507ef933a60b07ce72c268492fd4b6c3d012 Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Mon, 1 Jan 2024 19:37:45 +0800 Subject: [PATCH] Add recipient claim in SDK --- CHANGELOG.md | 1 + js/sdk-core/src/app-helper.ts | 12 +++++ js/sdk-solana/src/instruction.ts | 73 ++++++++++++++++++++++++++- js/sdk-solana/src/solana-transport.ts | 32 +++++++++++- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddfd372..d49ce64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Race Protocol: A multi-chain infrastructure for asymmetric competitive games ## Features - CLI: Update `publish` command. Now it receives the path to the WASM bundle instead of the Arweave URL to solana metadata. - Add optional `createProfileIfNeeded` to join options. +- SDK: Add `recipientClaim` and its solana implementation. ## Fixes - Transactor: Improve the retry mechanism for settle. diff --git a/js/sdk-core/src/app-helper.ts b/js/sdk-core/src/app-helper.ts index 0fef489f..d9271946 100644 --- a/js/sdk-core/src/app-helper.ts +++ b/js/sdk-core/src/app-helper.ts @@ -203,4 +203,16 @@ export class AppHelper { return new TokenWithBalance(t, balance); }); } + + /** + * Claim the fees collected by game. + * + * @param wallet - The wallet adapter to sign the transaction + * @param gameAddr - The address of game account. + */ + async claim(wallet: IWallet, gameAddr: string): Promise> { + const gameAccount = await this.#transport.getGameAccount(gameAddr); + if (gameAccount === undefined) throw new Error('Game account not found'); + return await this.#transport.recipientClaim(wallet, { recipientAddr: gameAccount?.recipientAddr }); + } } diff --git a/js/sdk-solana/src/instruction.ts b/js/sdk-solana/src/instruction.ts index 2cc603df..cce91cc3 100644 --- a/js/sdk-solana/src/instruction.ts +++ b/js/sdk-solana/src/instruction.ts @@ -1,10 +1,11 @@ -import { PublicKey, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; +import { PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'; import { publicKeyExt } from './utils'; import { PROGRAM_ID, METAPLEX_PROGRAM_ID, PLAYER_PROFILE_SEED } from './constants'; import { enums, field, serialize } from '@race-foundation/borsh'; import { Buffer } from 'buffer'; import { EntryType } from '@race-foundation/sdk-core'; +import { RecipientSlotOwnerAssigned, RecipientState } from './accounts'; // Instruction types @@ -21,6 +22,9 @@ export enum Instruction { UnregisterGame = 9, JoinGame = 10, PublishGame = 11, + CreateRecipient = 12, + AssignRecipient = 13, + RecipientClaim = 14, } // Instruction data definitations @@ -388,3 +392,70 @@ export function publishGame(opts: PublishGameOptions): TransactionInstruction { data, }); } + + +export type ClaimOpts = { + payerKey: PublicKey, + recipientKey: PublicKey, + recipientState: RecipientState, +}; + +export function claim(opts: ClaimOpts): TransactionInstruction { + const [pda, _] = PublicKey.findProgramAddressSync([opts.recipientKey.toBuffer()], PROGRAM_ID); + + let keys = [ + { + pubkey: opts.payerKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: opts.recipientKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: pda, + isSigner: false, + isWritable: false, + }, + { + pubkey: TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + for (const slot of opts.recipientState.slots) { + for (const slotShare of slot.shares) { + if (slotShare.owner instanceof RecipientSlotOwnerAssigned && slotShare.owner.addr === opts.payerKey) { + keys.push({ + pubkey: slot.stakeAddr, + isSigner: false, + isWritable: false, + }); + const ata = getAssociatedTokenAddressSync(slotShare.owner.addr, slot.tokenAddr); + keys.push({ + pubkey: ata, + isSigner: false, + isWritable: false, + }) + } + } + } + + if (keys.length === 5) { + return new Error('No slot to claim'); + } + + return new TransactionInstruction({ + keys, + programId: PROGRAM_ID, + data: Uint8Array.of(Instruction.RecipientClaim), + }); +} diff --git a/js/sdk-solana/src/solana-transport.ts b/js/sdk-solana/src/solana-transport.ts index 53c594f3..b33e6f64 100644 --- a/js/sdk-solana/src/solana-transport.ts +++ b/js/sdk-solana/src/solana-transport.ts @@ -252,8 +252,25 @@ export class SolanaTransport implements ITransport { throw new Error('unimplemented'); } - async recipientClaim(_wallet: IWallet, _params: RecipientClaimParams): Promise> { - throw new Error('unimplemented'); + async recipientClaim(wallet: IWallet, params: RecipientClaimParams): Promise> { + const payerKey = new PublicKey(wallet.recipientAddr); + const recipientKey = new PublicKey(params.recipientAddr); + const recipientState = await this._getRecipientState(recipientKey); + + if (recipientState === undefined) { + throw new Error('Recipient account not found'); + } + + const recipientClaimIx = instruction.claim({ + recipientKey, payerKey + }); + const tx = await makeTransaction(conn, playerKey); + + tx.add(recipientClaimIx); + + tx.partialSign(tempAccountKeypair); + + return await wallet.sendTransaction(tx, this.#conn); } async addCreateProfileIxToTransaction(tx: Transaction, wallet: IWallet, params: CreatePlayerProfileParams): Promise { @@ -650,6 +667,17 @@ export class SolanaTransport implements ITransport { return ret; } + async _getRecipientState(recipientKey: PublicKey): Promise { + const conn = this.#conn; + const recipientAccount = await conn.getAccountInfo(recipientKey); + if (recipientAccount !== null) { + const data = recipientAccount.data; + return RecipientState.deserialize(data); + } else { + return undefined; + } + } + async _getRegState(regKey: PublicKey): Promise { const conn = this.#conn; const regAccount = await conn.getAccountInfo(regKey);