UNPKG

@coinbase/cdp-sdk

Version:

SDK for interacting with the Coinbase Developer Platform Wallet API

669 lines (611 loc) 21.8 kB
import { constants, publicEncrypt } from "crypto"; import bs58 from "bs58"; import { CreateAccountOptions, ExportAccountOptions, GetAccountOptions, GetOrCreateAccountOptions, ImportAccountOptions, ListAccountsOptions, ListAccountsResult, ListTokenBalancesOptions, ListTokenBalancesResult, RequestFaucetOptions, SendTransactionOptions, SignatureResult, SignMessageOptions, SignTransactionOptions, SolanaClientInterface, UpdateSolanaAccountOptions, } from "./solana.types.js"; import { toSolanaAccount } from "../../accounts/solana/toSolanaAccount.js"; import { SolanaAccount } from "../../accounts/solana/types.js"; import { requestFaucet } from "../../actions/solana/requestFaucet.js"; import { sendTransaction, type SendTransactionResult, } from "../../actions/solana/sendTransaction.js"; import { signMessage } from "../../actions/solana/signMessage.js"; import { signTransaction, type SignTransactionResult, } from "../../actions/solana/signTransaction.js"; import { Analytics } from "../../analytics.js"; import { ImportAccountPublicRSAKey } from "../../constants.js"; import { UserInputValidationError } from "../../errors.js"; import { APIError } from "../../openapi-client/errors.js"; import { CdpOpenApiClient } from "../../openapi-client/index.js"; import { decryptWithPrivateKey, formatSolanaPrivateKey, generateExportEncryptionKeyPair, } from "../../utils/export.js"; /** * The namespace containing all Solana methods. */ export class SolanaClient implements SolanaClientInterface { /** * Creates a new Solana account. * * @param {CreateAccountOptions} options - Parameters for creating the Solana account. * @param {string} [options.name] - The name of the account. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the newly created account. * * @example **Without arguments** * ```ts * const account = await cdp.solana.createAccount(); * ``` * * @example **With a name** * ```ts * const account = await cdp.solana.createAccount({ name: "MyAccount" }); * ``` * * @example **With an idempotency key** * ```ts * const idempotencyKey = uuidv4(); * * // First call * await cdp.solana.createAccount({ idempotencyKey }); * * // Second call with the same idempotency key will return the same account * await cdp.solana.createAccount({ idempotencyKey }); * ``` */ async createAccount(options: CreateAccountOptions = {}): Promise<SolanaAccount> { Analytics.trackAction({ action: "create_account", accountType: "solana", }); return this._createAccountInternal(options); } /** * Exports a CDP Solana account's private key. * It is important to store the private key in a secure place after it's exported. * * @param {ExportAccountOptions} options - Parameters for exporting the Solana account. * @param {string} [options.address] - The address of the account. * @param {string} [options.name] - The name of the account. * * @returns A promise that resolves to the exported account's full 64-byte private key as a base58 encoded string. * * @example **With an address** * ```ts * const privateKey = await cdp.solana.exportAccount({ * address: "1234567890123456789012345678901234567890", * }); * ``` * * @example **With a name** * ```ts * const privateKey = await cdp.solana.exportAccount({ * name: "MyAccount", * }); * ``` */ async exportAccount(options: ExportAccountOptions): Promise<string> { Analytics.trackAction({ action: "export_account", accountType: "solana", }); const { publicKey, privateKey } = await generateExportEncryptionKeyPair(); const { encryptedPrivateKey } = await (async () => { if (options.address) { return CdpOpenApiClient.exportSolanaAccount( options.address, { exportEncryptionKey: publicKey, }, options.idempotencyKey, ); } if (options.name) { return CdpOpenApiClient.exportSolanaAccountByName( options.name, { exportEncryptionKey: publicKey, }, options.idempotencyKey, ); } throw new UserInputValidationError("Either address or name must be provided"); })(); const decryptedPrivateKey = decryptWithPrivateKey(privateKey, encryptedPrivateKey); return formatSolanaPrivateKey(decryptedPrivateKey); } /** * Imports a Solana account using a private key. * The private key will be encrypted before being stored securely. * * @param {ImportAccountOptions} options - Parameters for importing the Solana account. * @param {string} options.privateKey - The private key to import (32 or 64 bytes). Can be a base58 encoded string or raw bytes. * @param {string} [options.name] - The name of the account. * @param {string} [options.encryptionPublicKey] - The RSA public key for encrypting the private key. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the imported account. * * @example **Import with private key only** * ```ts * const account = await cdp.solana.importAccount({ * privateKey: "3Kzjw8qSxx8bQkV7EHrVFWYiPyNLbBVxtVe1Q5h2zKZY8DdcuT2dKxyz9kU5vQrP", * }); * ``` * * @example **Import with name** * ```ts * const account = await cdp.solana.importAccount({ * privateKey: "3Kzjw8qSxx8bQkV7EHrVFWYiPyNLbBVxtVe1Q5h2zKZY8DdcuT2dKxyz9kU5vQrP", * name: "ImportedAccount", * }); * ``` * * @example **Import with idempotency key** * ```ts * const idempotencyKey = uuidv4(); * * const account = await cdp.solana.importAccount({ * privateKey: "3Kzjw8qSxx8bQkV7EHrVFWYiPyNLbBVxtVe1Q5h2zKZY8DdcuT2dKxyz9kU5vQrP", * name: "ImportedAccount", * idempotencyKey, * }); * ``` */ async importAccount(options: ImportAccountOptions): Promise<SolanaAccount> { Analytics.trackAction({ action: "import_account", accountType: "solana", }); let privateKeyBytes: Uint8Array = new Uint8Array(); if (typeof options.privateKey === "string") { privateKeyBytes = bs58.decode(options.privateKey); } else { privateKeyBytes = options.privateKey; } if (privateKeyBytes.length !== 32 && privateKeyBytes.length !== 64) { throw new UserInputValidationError("Invalid private key length"); } if (privateKeyBytes.length === 64) { privateKeyBytes = privateKeyBytes.subarray(0, 32); } const encryptionPublicKey = options.encryptionPublicKey || ImportAccountPublicRSAKey; const encryptedPrivateKey = publicEncrypt( { key: encryptionPublicKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256", }, privateKeyBytes, ); const openApiAccount = await CdpOpenApiClient.importSolanaAccount( { encryptedPrivateKey: encryptedPrivateKey.toString("base64"), name: options.name, }, options.idempotencyKey, ); const account = toSolanaAccount(CdpOpenApiClient, { account: openApiAccount, }); Analytics.wrapObjectMethodsWithErrorTracking(account); return account; } /** * Gets a Solana account by its address. * * @param {GetAccountOptions} options - Parameters for getting the Solana account. * Either `address` or `name` must be provided. * If both are provided, lookup will be done by `address` and `name` will be ignored. * @param {string} [options.address] - The address of the account. * @param {string} [options.name] - The name of the account. * * @returns A promise that resolves to the account. * * @example **Get an account by address** * ```ts * const account = await cdp.solana.getAccount({ * address: "1234567890123456789012345678901234567890", * }); * ``` * * @example **Get an account by name** * ```ts * const account = await cdp.solana.getAccount({ * name: "MyAccount", * }); * ``` */ async getAccount(options: GetAccountOptions): Promise<SolanaAccount> { Analytics.trackAction({ action: "get_account", accountType: "solana", }); return this._getAccountInternal(options); } /** * Gets a Solana account by its address. * * @param {GetOrCreateAccountOptions} options - Parameters for getting or creating the Solana account. * @param {string} options.name - The name of the account. * * @returns A promise that resolves to the account. * * @example * ```ts * const account = await cdp.solana.getOrCreateAccount({ * name: "MyAccount", * }); * ``` */ async getOrCreateAccount(options: GetOrCreateAccountOptions): Promise<SolanaAccount> { Analytics.trackAction({ action: "get_or_create_account", accountType: "solana", }); try { const account = await this._getAccountInternal(options); return account; } catch (error) { // If it failed because the account doesn't exist, create it const doesAccountNotExist = error instanceof APIError && error.statusCode === 404; if (doesAccountNotExist) { try { const account = await this._createAccountInternal(options); return account; } catch (error) { // If it failed because the account already exists, get the existing account const doesAccountAlreadyExist = error instanceof APIError && error.statusCode === 409; if (doesAccountAlreadyExist) { const account = await this._getAccountInternal(options); return account; } throw error; } } throw error; } } /** * Lists all Solana accounts. * * @param {ListAccountsOptions} options - Parameters for listing the Solana accounts. * @param {number} [options.pageSize] - The number of accounts to return. * @param {string} [options.pageToken] - The page token to begin listing from. * This is obtained by previous calls to this method. * * @returns A promise that resolves to an array of Solana account instances. * * @example **Without arguments** * ```ts * const accounts = await cdp.solana.listAccounts(); * ``` * * @example **With pagination** * ```ts * let page = await cdp.solana.listAccounts(); * * while (page.nextPageToken) { * page = await cdp.solana.listAccounts({ pageToken: page.nextPageToken }); * } * * page.accounts.forEach(account => console.log(account)); * ``` * } * ``` */ async listAccounts(options: ListAccountsOptions = {}): Promise<ListAccountsResult> { Analytics.trackAction({ action: "list_accounts", accountType: "solana", }); const solAccounts = await CdpOpenApiClient.listSolanaAccounts({ pageSize: options.pageSize, pageToken: options.pageToken, }); return { accounts: solAccounts.accounts.map(account => { const solanaAccount = toSolanaAccount(CdpOpenApiClient, { account, }); Analytics.wrapObjectMethodsWithErrorTracking(solanaAccount); return solanaAccount; }), nextPageToken: solAccounts.nextPageToken, }; } /** * Requests funds from a Solana faucet. * * @param {RequestFaucetOptions} options - Parameters for requesting funds from the Solana faucet. * @param {string} options.address - The address to request funds for. * @param {string} options.token - The token to request funds for. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the transaction signature. * * @example * ```ts * const signature = await cdp.solana.requestFaucet({ * address: "1234567890123456789012345678901234567890", * token: "sol", * }); * ``` */ async requestFaucet(options: RequestFaucetOptions): Promise<SignatureResult> { Analytics.trackAction({ action: "request_faucet", accountType: "solana", }); return requestFaucet(CdpOpenApiClient, options); } /** * Signs a message. * * @param {SignMessageOptions} options - Parameters for signing the message. * @param {string} options.address - The address to sign the message for. * @param {string} options.message - The message to sign. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the signature. * * @example * ```ts * // Create a Solana account * const account = await cdp.solana.createAccount(); * * // When you want to sign a message, you can do so by address * const signature = await cdp.solana.signMessage({ * address: account.address, * message: "Hello, world!", * }); * ``` */ async signMessage(options: SignMessageOptions): Promise<SignatureResult> { Analytics.trackAction({ action: "sign_message", accountType: "solana", }); return signMessage(CdpOpenApiClient, options); } /** * Signs a transaction. * * @param {SignTransactionOptions} options - Parameters for signing the transaction. * @param {string} options.address - The address to sign the transaction for. * @param {string} options.transaction - The transaction to sign. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the signature. * * @example * ```ts * // Create a Solana account * const account = await cdp.solana.createAccount(); * * // Add your transaction instructions here * const transaction = new Transaction() * * // Make sure to set requireAllSignatures to false, since signing will be done through the API * const serializedTransaction = transaction.serialize({ * requireAllSignatures: false, * }); * * // Base64 encode the serialized transaction * const transaction = Buffer.from(serializedTransaction).toString("base64"); * * // When you want to sign a transaction, you can do so by address and base64 encoded transaction * const signature = await cdp.solana.signTransaction({ * address: account.address, * transaction, * }); * ``` */ async signTransaction(options: SignTransactionOptions): Promise<SignTransactionResult> { Analytics.trackAction({ action: "sign_transaction", accountType: "solana", }); return signTransaction(CdpOpenApiClient, options); } /** * Updates a CDP Solana account. * * @param {UpdateSolanaAccountOptions} [options] - Optional parameters for creating the account. * @param {string} options.address - The address of the account to update * @param {UpdateSolanaAccountBody} options.update - An object containing account fields to update. * @param {string} [options.update.name] - The new name for the account. * @param {string} [options.update.accountPolicy] - The ID of a Policy to apply to the account. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the updated account. * * @example **With a name** * ```ts * const account = await cdp.sol.updateAccount({ address: "...", update: { name: "New Name" } }); * ``` * * @example **With an account policy** * ```ts * const account = await cdp.sol.updateAccount({ address: "...", update: { accountPolicy: "73bcaeeb-d7af-4615-b064-42b5fe83a31e" } }); * ``` * * @example **With an idempotency key** * ```ts * const idempotencyKey = uuidv4(); * * // First call * await cdp.sol.updateAccount({ * address: "0x...", * update: { accountPolicy: "73bcaeeb-d7af-4615-b064-42b5fe83a31e" }, * idempotencyKey, * }); * * // Second call with the same idempotency key will not update * await cdp.sol.updateAccount({ * address: '0x...', * update: { name: "" }, * idempotencyKey, * }); * ``` */ async updateAccount(options: UpdateSolanaAccountOptions): Promise<SolanaAccount> { Analytics.trackAction({ action: "update_account", accountType: "solana", }); const openApiAccount = await CdpOpenApiClient.updateSolanaAccount( options.address, options.update, options.idempotencyKey, ); const account = toSolanaAccount(CdpOpenApiClient, { account: openApiAccount, }); Analytics.wrapObjectMethodsWithErrorTracking(account); return account; } /** * Sends a Solana transaction using the Coinbase API. * * @param {SendTransactionOptions} options - Parameters for sending the Solana transaction. * @param {string} options.network - The network to send the transaction to. * @param {string} options.transaction - The base64 encoded transaction to send. * @param {string} [options.idempotencyKey] - An idempotency key. * * @returns A promise that resolves to the transaction result. * * @example * ```ts * const signature = await cdp.solana.sendTransaction({ * network: "solana-devnet", * transaction: "...", * }); * ``` */ async sendTransaction(options: SendTransactionOptions): Promise<SendTransactionResult> { Analytics.trackAction({ action: "send_transaction", accountType: "solana", properties: { network: options.network, }, }); return sendTransaction(CdpOpenApiClient, options); } /** * Lists the token balances for a Solana account. * * @param {ListTokenBalancesOptions} options - Parameters for listing the Solana token balances. * @param {string} options.address - The address of the account to list token balances for. * @param {string} [options.network] - The network to list token balances for. Defaults to "solana". * @param {number} [options.pageSize] - The number of token balances to return. * @param {string} [options.pageToken] - The page token to begin listing from. * This is obtained by previous calls to this method. * * @returns A promise that resolves to an array of Solana token balance instances. * * @example * ```ts * const balances = await cdp.solana.listTokenBalances({ address: "...", network: "solana-devnet" }); * ``` */ async listTokenBalances(options: ListTokenBalancesOptions): Promise<ListTokenBalancesResult> { Analytics.trackAction({ action: "list_token_balances", accountType: "solana", properties: { network: options.network, }, }); const tokenBalances = await CdpOpenApiClient.listSolanaTokenBalances( options.network || "solana", options.address, { pageSize: options.pageSize, pageToken: options.pageToken, }, ); return { balances: tokenBalances.balances.map(balance => { return { amount: { amount: BigInt(balance.amount.amount), decimals: balance.amount.decimals, }, token: { mintAddress: balance.token.mintAddress, name: balance.token.name, symbol: balance.token.symbol, }, }; }), nextPageToken: tokenBalances.nextPageToken, }; } /** * Internal method to create a Solana account without tracking analytics. * Used internally by composite operations to avoid double-counting. * * @param {CreateAccountOptions} options - Parameters for creating the account. * @returns {Promise<SolanaAccount>} A promise that resolves to the newly created account. */ private async _createAccountInternal(options: CreateAccountOptions = {}): Promise<SolanaAccount> { const openApiAccount = await CdpOpenApiClient.createSolanaAccount( { name: options.name, accountPolicy: options.accountPolicy, }, options.idempotencyKey, ); const account = toSolanaAccount(CdpOpenApiClient, { account: openApiAccount, }); Analytics.wrapObjectMethodsWithErrorTracking(account); return account; } /** * Internal method to get a Solana account without tracking analytics. * Used internally by composite operations to avoid double-counting. * * @param {GetAccountOptions} options - Parameters for getting the account. * @returns {Promise<SolanaAccount>} A promise that resolves to the account. */ private async _getAccountInternal(options: GetAccountOptions): Promise<SolanaAccount> { const openApiAccount = await (() => { if (options.address) { return CdpOpenApiClient.getSolanaAccount(options.address); } if (options.name) { return CdpOpenApiClient.getSolanaAccountByName(options.name); } throw new UserInputValidationError("Either address or name must be provided"); })(); const account = toSolanaAccount(CdpOpenApiClient, { account: openApiAccount, }); Analytics.wrapObjectMethodsWithErrorTracking(account); return account; } }