Skip to content

Commit 7b301db

Browse files
committed
feat(sdk-coin-sol): added api key for full node
txn COIN-4967 Add API key support to Sol module for recovery and node requests. This enables users to provide their own Alchemy API keys when making Solana node requests to improve rate limits and reliability. The apiKey parameter is optional and falls back to standard node URLs when not provided. TICKET: COIN-4967
1 parent 164ddf6 commit 7b301db

File tree

3 files changed

+159
-101
lines changed

3 files changed

+159
-101
lines changed

modules/sdk-coin-sol/src/sol.ts

Lines changed: 130 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export interface SolRecoveryOptions extends MPCRecoveryOptions {
157157
// destination address where token should be sent before closing the ATA address
158158
recoveryDestinationAtaAddress?: string;
159159
programId?: string; // programId of the token
160+
apiKey?: string; // API key for node requests
160161
}
161162

162163
export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecoveryOptions {
@@ -165,6 +166,7 @@ export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecover
165166
secretKey: string;
166167
};
167168
tokenContractAddress?: string;
169+
apiKey?: string; // API key for node requests
168170
}
169171

170172
const HEX_REGEX = /^[0-9a-fA-F]+$/;
@@ -534,16 +536,22 @@ export class Sol extends BaseCoin {
534536
});
535537
}
536538

537-
protected getPublicNodeUrl(): string {
539+
protected getPublicNodeUrl(apiKey?: string): string {
540+
if (apiKey) {
541+
return Environments[this.bitgo.getEnv()].solAlchemyNodeUrl + `/${apiKey}`;
542+
}
538543
return Environments[this.bitgo.getEnv()].solNodeUrl;
539544
}
540545

541546
/**
542547
* Make a request to one of the public SOL nodes available
543548
* @param params.payload
544549
*/
545-
protected async getDataFromNode(params: { payload?: Record<string, unknown> }): Promise<request.Response> {
546-
const nodeUrl = this.getPublicNodeUrl();
550+
protected async getDataFromNode(
551+
params: { payload?: Record<string, unknown> },
552+
apiKey?: string
553+
): Promise<request.Response> {
554+
const nodeUrl = this.getPublicNodeUrl(apiKey);
547555
try {
548556
return await request.post(nodeUrl).send(params.payload);
549557
} catch (e) {
@@ -552,92 +560,107 @@ export class Sol extends BaseCoin {
552560
throw new Error(`Unable to call endpoint: '/' from node: ${nodeUrl}`);
553561
}
554562

555-
protected async getBlockhash(): Promise<string> {
556-
const response = await this.getDataFromNode({
557-
payload: {
558-
id: '1',
559-
jsonrpc: '2.0',
560-
method: 'getLatestBlockhash',
561-
params: [
562-
{
563-
commitment: 'finalized',
564-
},
565-
],
563+
protected async getBlockhash(apiKey?: string): Promise<string> {
564+
const response = await this.getDataFromNode(
565+
{
566+
payload: {
567+
id: '1',
568+
jsonrpc: '2.0',
569+
method: 'getLatestBlockhash',
570+
params: [
571+
{
572+
commitment: 'finalized',
573+
},
574+
],
575+
},
566576
},
567-
});
577+
apiKey
578+
);
568579
if (response.status !== 200) {
569580
throw new Error('Account not found');
570581
}
571582

572583
return response.body.result.value.blockhash;
573584
}
574585

575-
protected async getFeeForMessage(message: string): Promise<number> {
576-
const response = await this.getDataFromNode({
577-
payload: {
578-
id: '1',
579-
jsonrpc: '2.0',
580-
method: 'getFeeForMessage',
581-
params: [
582-
message,
583-
{
584-
commitment: 'finalized',
585-
},
586-
],
586+
protected async getFeeForMessage(message: string, apiKey?: string): Promise<number> {
587+
const response = await this.getDataFromNode(
588+
{
589+
payload: {
590+
id: '1',
591+
jsonrpc: '2.0',
592+
method: 'getFeeForMessage',
593+
params: [
594+
message,
595+
{
596+
commitment: 'finalized',
597+
},
598+
],
599+
},
587600
},
588-
});
601+
apiKey
602+
);
589603
if (response.status !== 200) {
590604
throw new Error('Account not found');
591605
}
592606

593607
return response.body.result.value;
594608
}
595609

596-
protected async getRentExemptAmount(): Promise<number> {
597-
const response = await this.getDataFromNode({
598-
payload: {
599-
jsonrpc: '2.0',
600-
id: '1',
601-
method: 'getMinimumBalanceForRentExemption',
602-
params: [165],
610+
protected async getRentExemptAmount(apiKey?: string): Promise<number> {
611+
const response = await this.getDataFromNode(
612+
{
613+
payload: {
614+
jsonrpc: '2.0',
615+
id: '1',
616+
method: 'getMinimumBalanceForRentExemption',
617+
params: [165],
618+
},
603619
},
604-
});
620+
apiKey
621+
);
605622
if (response.status !== 200 || response.error) {
606623
throw new Error(JSON.stringify(response.error));
607624
}
608625

609626
return response.body.result;
610627
}
611628

612-
protected async getAccountBalance(pubKey: string): Promise<number> {
613-
const response = await this.getDataFromNode({
614-
payload: {
615-
id: '1',
616-
jsonrpc: '2.0',
617-
method: 'getBalance',
618-
params: [pubKey],
629+
protected async getAccountBalance(pubKey: string, apiKey?: string): Promise<number> {
630+
const response = await this.getDataFromNode(
631+
{
632+
payload: {
633+
id: '1',
634+
jsonrpc: '2.0',
635+
method: 'getBalance',
636+
params: [pubKey],
637+
},
619638
},
620-
});
639+
apiKey
640+
);
621641
if (response.status !== 200) {
622642
throw new Error('Account not found');
623643
}
624644
return response.body.result.value;
625645
}
626646

627-
protected async getAccountInfo(pubKey: string): Promise<SolDurableNonceFromNode> {
628-
const response = await this.getDataFromNode({
629-
payload: {
630-
id: '1',
631-
jsonrpc: '2.0',
632-
method: 'getAccountInfo',
633-
params: [
634-
pubKey,
635-
{
636-
encoding: 'jsonParsed',
637-
},
638-
],
647+
protected async getAccountInfo(pubKey: string, apiKey?: string): Promise<SolDurableNonceFromNode> {
648+
const response = await this.getDataFromNode(
649+
{
650+
payload: {
651+
id: '1',
652+
jsonrpc: '2.0',
653+
method: 'getAccountInfo',
654+
params: [
655+
pubKey,
656+
{
657+
encoding: 'jsonParsed',
658+
},
659+
],
660+
},
639661
},
640-
});
662+
apiKey
663+
);
641664
if (response.status !== 200) {
642665
throw new Error('Account not found');
643666
}
@@ -647,26 +670,29 @@ export class Sol extends BaseCoin {
647670
};
648671
}
649672

650-
protected async getTokenAccountsByOwner(pubKey = '', programId = ''): Promise<[] | TokenAccount[]> {
651-
const response = await this.getDataFromNode({
652-
payload: {
653-
id: '1',
654-
jsonrpc: '2.0',
655-
method: 'getTokenAccountsByOwner',
656-
params: [
657-
pubKey,
658-
{
659-
programId:
660-
programId.toString().toLowerCase() === TOKEN_2022_PROGRAM_ID.toString().toLowerCase()
661-
? TOKEN_2022_PROGRAM_ID.toString()
662-
: TOKEN_PROGRAM_ID.toString(),
663-
},
664-
{
665-
encoding: 'jsonParsed',
666-
},
667-
],
673+
protected async getTokenAccountsByOwner(pubKey = '', programId = '', apiKey?: string): Promise<[] | TokenAccount[]> {
674+
const response = await this.getDataFromNode(
675+
{
676+
payload: {
677+
id: '1',
678+
jsonrpc: '2.0',
679+
method: 'getTokenAccountsByOwner',
680+
params: [
681+
pubKey,
682+
{
683+
programId:
684+
programId.toString().toLowerCase() === TOKEN_2022_PROGRAM_ID.toString().toLowerCase()
685+
? TOKEN_2022_PROGRAM_ID.toString()
686+
: TOKEN_PROGRAM_ID.toString(),
687+
},
688+
{
689+
encoding: 'jsonParsed',
690+
},
691+
],
692+
},
668693
},
669-
});
694+
apiKey
695+
);
670696
if (response.status !== 200) {
671697
throw new Error('Account not found');
672698
}
@@ -682,20 +708,23 @@ export class Sol extends BaseCoin {
682708
return [];
683709
}
684710

685-
protected async getTokenAccountInfo(pubKey: string): Promise<TokenAccount> {
686-
const response = await this.getDataFromNode({
687-
payload: {
688-
id: '1',
689-
jsonrpc: '2.0',
690-
method: 'getAccountInfo',
691-
params: [
692-
pubKey,
693-
{
694-
encoding: 'jsonParsed',
695-
},
696-
],
711+
protected async getTokenAccountInfo(pubKey: string, apiKey?: string): Promise<TokenAccount> {
712+
const response = await this.getDataFromNode(
713+
{
714+
payload: {
715+
id: '1',
716+
jsonrpc: '2.0',
717+
method: 'getAccountInfo',
718+
params: [
719+
pubKey,
720+
{
721+
encoding: 'jsonParsed',
722+
},
723+
],
724+
},
697725
},
698-
});
726+
apiKey
727+
);
699728
if (response.status !== 200) {
700729
throw new Error('Account not found');
701730
}
@@ -791,21 +820,21 @@ export class Sol extends BaseCoin {
791820
const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
792821
const bs58EncodedPublicKey = new SolKeyPair({ pub: accountId }).getAddress();
793822

794-
balance = await this.getAccountBalance(bs58EncodedPublicKey);
823+
balance = await this.getAccountBalance(bs58EncodedPublicKey, params.apiKey);
795824

796825
const factory = this.getBuilder();
797826
const walletCoin = this.getChain();
798827

799828
let txBuilder;
800-
let blockhash = await this.getBlockhash();
829+
let blockhash = await this.getBlockhash(params.apiKey);
801830
let rentExemptAmount;
802831
let authority = '';
803832
let totalFee = new BigNumber(0);
804833
let totalFeeForTokenRecovery = new BigNumber(0);
805834

806835
// check for possible token recovery, recover the token provide by user
807836
if (params.tokenContractAddress) {
808-
const tokenAccounts = await this.getTokenAccountsByOwner(bs58EncodedPublicKey, params.programId);
837+
const tokenAccounts = await this.getTokenAccountsByOwner(bs58EncodedPublicKey, params.programId, params.apiKey);
809838
if (tokenAccounts.length !== 0) {
810839
// there exists token accounts on the given address, but need to check certain conditions:
811840
// 1. if there is a recoverable balance
@@ -826,7 +855,7 @@ export class Sol extends BaseCoin {
826855
}
827856

828857
if (recovereableTokenAccounts.length !== 0) {
829-
rentExemptAmount = await this.getRentExemptAmount();
858+
rentExemptAmount = await this.getRentExemptAmount(params.apiKey);
830859

831860
txBuilder = factory
832861
.getTokenTransferBuilder()
@@ -838,7 +867,8 @@ export class Sol extends BaseCoin {
838867
// need to get all token accounts of the recipient address and need to create them if they do not exist
839868
const recipientTokenAccounts = await this.getTokenAccountsByOwner(
840869
params.recoveryDestination,
841-
params.programId
870+
params.programId,
871+
params.apiKey
842872
);
843873

844874
for (const tokenAccount of recovereableTokenAccounts) {
@@ -894,7 +924,7 @@ export class Sol extends BaseCoin {
894924
}
895925

896926
if (params.durableNonce) {
897-
const durableNonceInfo = await this.getAccountInfo(params.durableNonce.publicKey);
927+
const durableNonceInfo = await this.getAccountInfo(params.durableNonce.publicKey, params.apiKey);
898928
blockhash = durableNonceInfo.blockhash;
899929
authority = durableNonceInfo.authority;
900930

@@ -908,7 +938,7 @@ export class Sol extends BaseCoin {
908938
const unsignedTransactionWithoutFee = (await txBuilder.build()) as Transaction;
909939
const serializedMessage = unsignedTransactionWithoutFee.solTransaction.serializeMessage().toString('base64');
910940

911-
const baseFee = await this.getFeeForMessage(serializedMessage);
941+
const baseFee = await this.getFeeForMessage(serializedMessage, params.apiKey);
912942
const feePerSignature = params.durableNonce ? baseFee / 2 : baseFee;
913943
totalFee = totalFee.plus(new BigNumber(baseFee));
914944
totalFeeForTokenRecovery = totalFeeForTokenRecovery.plus(new BigNumber(baseFee));
@@ -1118,7 +1148,7 @@ export class Sol extends BaseCoin {
11181148
throw new Error('invalid recoveryDestinationAtaAddress');
11191149
}
11201150

1121-
blockhash = await this.getBlockhash();
1151+
blockhash = await this.getBlockhash(params.apiKey);
11221152

11231153
txBuilder = factory
11241154
.getTokenTransferBuilder()
@@ -1128,7 +1158,7 @@ export class Sol extends BaseCoin {
11281158
.feePayer(bs58EncodedPublicKey);
11291159
const unsignedTransaction = (await txBuilder.build()) as Transaction;
11301160
const serializedMessage = unsignedTransaction.solTransaction.serializeMessage().toString('base64');
1131-
const feePerSignature = await this.getFeeForMessage(serializedMessage);
1161+
const feePerSignature = await this.getFeeForMessage(serializedMessage, params.apiKey);
11321162
const baseFee = params.durableNonce ? feePerSignature * 2 : feePerSignature;
11331163
const totalFee = new BigNumber(baseFee);
11341164
if (totalFee.gt(accountBalance)) {
@@ -1159,7 +1189,7 @@ export class Sol extends BaseCoin {
11591189

11601190
// after recovering the token amount, attempting to close the ATA address
11611191
if (params.closeAtaAddress) {
1162-
blockhash = await this.getBlockhash();
1192+
blockhash = await this.getBlockhash(params.apiKey);
11631193

11641194
const ataCloseBuilder = () => {
11651195
const txBuilder = factory.getCloseAtaInitializationBuilder();
@@ -1316,6 +1346,7 @@ export class Sol extends BaseCoin {
13161346
secretKey: params.durableNonces.secretKey,
13171347
},
13181348
tokenContractAddress: params.tokenContractAddress,
1349+
apiKey: params.apiKey,
13191350
};
13201351

13211352
let recoveryTransaction;

0 commit comments

Comments
 (0)