Skip to content

Commit

Permalink
Txn relayer and v0 transaction sign support added (#48)
Browse files Browse the repository at this point in the history
* v0 txn support add on signAndSendTransaction

* txn relayer added

* 0.2.29
  • Loading branch information
impin2rex authored Nov 18, 2023
1 parent a6c8960 commit f491f8a
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 15 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.2.28",
"version": "0.2.29",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
62 changes: 62 additions & 0 deletions src/api/txn-relayer-client.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string> {
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<SendTransactionResp[]> {
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;
}
}
3 changes: 3 additions & 0 deletions src/utils/shyft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StorageClient,
CallbackClient,
RpcClient,
TxnRelayerClient,
} from '@/api';
import { ShyftConfig } from '@/utils';
import { SemiCustodialWalletClient } from '@/api/semi-custodial-wallet-client';
Expand All @@ -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;
Expand All @@ -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);
Expand Down
30 changes: 21 additions & 9 deletions src/utils/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Keypair,
Transaction,
Signer,
VersionedTransaction,
} from '@solana/web3.js';
import { decode } from 'bs58';

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -76,10 +75,14 @@ export async function signAndSendTransaction(
export async function partialSignTransactionWithPrivateKeys(
encodedTransaction: string,
privateKeys: string[]
): Promise<Transaction> {
): Promise<Transaction | VersionedTransaction> {
const recoveredTransaction = getRawTransaction(encodedTransaction);
const signers = getSignersFromPrivateKeys(privateKeys);
recoveredTransaction.partialSign(...signers);
if (recoveredTransaction instanceof VersionedTransaction) {
recoveredTransaction.sign(signers);
} else {
recoveredTransaction.partialSign(...signers);
}
return recoveredTransaction;
}

Expand All @@ -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;
}
36 changes: 36 additions & 0 deletions tests/txn-relayer-client.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit f491f8a

Please sign in to comment.