diff --git a/README.md b/README.md index 761220a..28f9df9 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ The Shyft SDK currently supports the following clients: - `candyMachine`: Candy Machine APIs - `marketplace`: Marketplace APIs - `transaction`: Transation APIs -- `storage`: Storage APIs such as uploading asset or metadata and get IPFS uri. -- `semiCustodialWallet`: A simple in-app crypto wallet to securely and quickly onboard non-native crypto users to web3 dApps. -- `callback`: Get real time updates on addresses for your users. +- `txnRelayer`: Transaction Relayer, allows you to seamlessly enable gas-less transactions for your users +- `storage`: Storage APIs such as uploading asset or metadata and get IPFS uri +- `semiCustodialWallet`: A simple in-app crypto wallet to securely and quickly onboard non-native crypto users to web3 dApps +- `callback`: Get real time updates on addresses for your users - `rpc`: [Get access to DAS API (currently only works with `mainnet-beta` cluster) 🆕](#rpc) ### Shyft Wallet APIs @@ -265,6 +266,16 @@ const transactions = await shyft.transaction.history({ console.dir(transactions, { depth: null }); ``` +### Transaction Relayer APIs + +It first creates a custodial wallet which gets mapped to your Shyft API key. On creation, it returns the wallet address associated with you SHYFT API key. You have to use this wallet address as,fee_payer while constructing your transactions. Then, you can send the transactions that need to be signed on the relayer’s sign endpoint. Relayer will retrieve the credentials associated with your API key, sign the transaction and send it to the blockchain. + +Txn Relayer namespace: + +- `getOrCreate()`: Get or create a new transaction relayer. +- `sign()`: Sign and send a transaction using the relayer. Takes `encoded_transaction` and network as input request parameters. +- `signMany()`: Sign and send multiple transactions using the relayer. Takes `encoded_transactions` and network as input request parameters. + ### Storage APIs Your gateway to decentralized storage diff --git a/package-lock.json b/package-lock.json index c4964be..b20f44c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shyft-to/js", - "version": "0.2.28", + "version": "0.2.29", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shyft-to/js", - "version": "0.2.28", + "version": "0.2.29", "license": "MIT", "dependencies": { "@solana/web3.js": "^1.74.0", diff --git a/package.json b/package.json index 49d99bb..697f084 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.2.28", + "version": "0.2.29", "license": "MIT", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/src/api/index.ts b/src/api/index.ts index 867893b..be8695f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -4,6 +4,7 @@ export * from './token-client'; export * from './candy-machine-client'; export * from './marketplace-client'; export * from './transaction-client'; +export * from './txn-relayer-client'; export * from './storage-client'; export * from './callback-client'; export * from './rpc-client'; diff --git a/src/api/txn-relayer-client.ts b/src/api/txn-relayer-client.ts new file mode 100644 index 0000000..c9b8ab7 --- /dev/null +++ b/src/api/txn-relayer-client.ts @@ -0,0 +1,62 @@ +import { ShyftConfig } from '@/utils'; +import { restApiCall } from '@/utils'; +import { Network, SendTransactionResp, TxnCommitment } from '@/types'; + +export class TxnRelayerClient { + constructor(private readonly config: ShyftConfig) {} + + async getOrCreate(): Promise { + const data = await restApiCall(this.config.apiKey, { + method: 'post', + url: 'txn_relayer/create', + }); + const wallet = data.result.wallet as string; + return wallet; + } + + async sign(input: { + network?: Network; + encodedTransaction: string; + }): Promise { + const reqBody = { + network: input.network ?? this.config.network, + encoded_transaction: input.encodedTransaction, + }; + + const data = await restApiCall(this.config.apiKey, { + method: 'post', + url: 'txn_relayer/sign', + data: reqBody, + }); + const result = data.result?.tx as string; + return result; + } + + async signMany(input: { + network?: Network; + encodedTransactions: string[]; + commitment?: TxnCommitment; + }): Promise { + if ( + input.encodedTransactions.length > 50 || + input.encodedTransactions.length < 1 + ) { + throw new Error('allowed between 1 to 50: encodedTransactions'); + } + const reqBody = { + network: input.network ?? this.config.network, + encoded_transactions: input.encodedTransactions, + }; + if (input?.commitment) { + reqBody['commitment'] = input.commitment; + } + + const data = await restApiCall(this.config.apiKey, { + method: 'post', + url: 'txn_relayer/sign_many', + data: reqBody, + }); + const result = data.result as SendTransactionResp[]; + return result; + } +} diff --git a/src/utils/shyft.ts b/src/utils/shyft.ts index 384b115..08726cc 100644 --- a/src/utils/shyft.ts +++ b/src/utils/shyft.ts @@ -9,6 +9,7 @@ import { StorageClient, CallbackClient, RpcClient, + TxnRelayerClient, } from '@/api'; import { ShyftConfig } from '@/utils'; import { SemiCustodialWalletClient } from '@/api/semi-custodial-wallet-client'; @@ -24,6 +25,7 @@ export class ShyftSdk { readonly candyMachine: CandyMachineClient; readonly marketplace: MarketplaceClient; readonly transaction: TransactionClient; + readonly txnRelayer: TxnRelayerClient; readonly storage: StorageClient; readonly semiCustodialWallet: SemiCustodialWalletClient; readonly callback: CallbackClient; @@ -39,6 +41,7 @@ export class ShyftSdk { this.candyMachine = new CandyMachineClient(this.config); this.marketplace = new MarketplaceClient(this.config); this.transaction = new TransactionClient(this.config); + this.txnRelayer = new TxnRelayerClient(this.config); this.storage = new StorageClient(this.config); this.semiCustodialWallet = new SemiCustodialWalletClient(this.config); this.callback = new CallbackClient(this.config); diff --git a/src/utils/signer.ts b/src/utils/signer.ts index 18c527c..543ce90 100644 --- a/src/utils/signer.ts +++ b/src/utils/signer.ts @@ -4,6 +4,7 @@ import { Keypair, Transaction, Signer, + VersionedTransaction, } from '@solana/web3.js'; import { decode } from 'bs58'; @@ -33,9 +34,7 @@ export async function signAndSendTransactionWithPrivateKeys( privateKeys ); - const signature = await connection.sendRawTransaction( - signedTxn.serialize({ requireAllSignatures: false }) - ); + const signature = await connection.sendRawTransaction(signedTxn.serialize()); return signature; } @@ -76,10 +75,14 @@ export async function signAndSendTransaction( export async function partialSignTransactionWithPrivateKeys( encodedTransaction: string, privateKeys: string[] -): Promise { +): Promise { const recoveredTransaction = getRawTransaction(encodedTransaction); const signers = getSignersFromPrivateKeys(privateKeys); - recoveredTransaction.partialSign(...signers); + if (recoveredTransaction instanceof VersionedTransaction) { + recoveredTransaction.sign(signers); + } else { + recoveredTransaction.partialSign(...signers); + } return recoveredTransaction; } @@ -90,9 +93,18 @@ function getSignersFromPrivateKeys(privateKeys: string[]): Signer[] { }); } -function getRawTransaction(encodedTransaction: string): Transaction { - const recoveredTransaction = Transaction.from( - Buffer.from(encodedTransaction, 'base64') - ); +function getRawTransaction( + encodedTransaction: string +): Transaction | VersionedTransaction { + let recoveredTransaction: Transaction | VersionedTransaction; + try { + recoveredTransaction = Transaction.from( + Buffer.from(encodedTransaction, 'base64') + ); + } catch (error) { + recoveredTransaction = VersionedTransaction.deserialize( + Buffer.from(encodedTransaction, 'base64') + ); + } return recoveredTransaction; } diff --git a/tests/txn-relayer-client.spec.ts b/tests/txn-relayer-client.spec.ts new file mode 100644 index 0000000..b03279d --- /dev/null +++ b/tests/txn-relayer-client.spec.ts @@ -0,0 +1,36 @@ +import 'dotenv/config'; +import { ShyftSdk } from '@/index'; +import { Network } from '@/types'; + +const shyft = new ShyftSdk({ + apiKey: process.env.API_KEY as string, + network: Network.Devnet, +}); + +describe('Transaction Relayer client test', () => { + it('get or create relay wallet', async () => { + const wallet = await shyft.txnRelayer.getOrCreate(); + expect(typeof wallet).toBe('string'); + }); + + it('sign transaction', async () => { + const signature = await shyft.txnRelayer.sign({ + network: Network.Devnet, + encodedTransaction: + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQACBc1gsUE/MCpM0cXSNRdR/uvKkw28CfT/UmFjeQO1P3RTZzmxZaKzGUFs516b0/MuXMRFJmwuLzL221Zha9e6/yURWkj1VBal8ukhGe3QD/WcUgoNM/i/hJJbB20A/LLeuAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpxkeHlDF/XZwFVODF42SUvNKnI6z+t1y+mcP3Eqbp1vLLapjYzOUB2ZYSvnp0kYl1UD4ZcXxiFaQEf+mE1ygN6wEDBAEEAgAKDEBCDwAAAAAACQA=', + }); + expect(typeof signature).toBe('string'); + }); + + it('sign multiple transactions', async () => { + const response = await shyft.txnRelayer.signMany({ + network: Network.Devnet, + encodedTransactions: [ + 'AflRiSHDT+mkiqNqJ6MsY5cqITOcZ37+txZQqqYzazphSa/VBhFcFibFzi2rQ8IsaQkgBDHznqabolOzA/mNlAUBAAEDGMqfUcVHHu2+lyU5gx9wU2rGxk2NoSXtodMSdev+DxVALAeVfwkorAqlrAeN8dOn8Jeu6H4u3ofHzGz4FntQPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR9uaAIxMGKAuXPKB9d9mAA3oMZ/GRc3kbW+YxugY/jcBAgIAAQwCAAAAMBsPAAAAAAA=', + 'AW+cdqIs1kmQzpWS9YjXdx8eHqqv8IQsurH88P54ZUvbdysyCnsEpxEt2Y2xZ8+yCP/jj8hFyih8NZiWUqnb3QUBAAEDGMqfUcVHHu2+lyU5gx9wU2rGxk2NoSXtodMSdev+DxVALAeVfwkorAqlrAeN8dOn8Jeu6H4u3ofHzGz4FntQPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR9uaAIxMGKAuXPKB9d9mAA3oMZ/GRc3kbW+YxugY/jcBAgIAAQwCAAAAQEIPAAAAAAA=', + ], + commitment: 'confirmed', + }); + expect(typeof response).toBe('object'); + }); +});