From 5484d704de3d22f092564575212119ce41afe6e2 Mon Sep 17 00:00:00 2001 From: John Peterson Date: Thu, 6 Jun 2024 16:03:39 -0700 Subject: [PATCH] [PSDK-247] ServerSigner Class Impl + ServerSigner.getDefault() --- CHANGELOG.md | 4 +- src/client/api.ts | 241 ++++++++++++++++++++++- src/coinbase/coinbase.ts | 2 + src/coinbase/server_signer.ts | 65 ++++++ src/coinbase/tests/server_signer_test.ts | 95 +++++++++ src/coinbase/tests/utils.ts | 4 + src/coinbase/types.ts | 22 +++ 7 files changed, 424 insertions(+), 9 deletions(-) create mode 100644 src/coinbase/server_signer.ts create mode 100644 src/coinbase/tests/server_signer_test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eddb46a..5bbc95af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Added Base Mainnet network support +- `ServerSigner` object +- Ability to get default Server-Signer ### Changed @@ -38,4 +40,4 @@ Initial release of the Coinbase NodeJS. - API HTTP debugging - User object and getDefaultUser - Individual private key export -- Error specifications \ No newline at end of file +- Error specifications diff --git a/src/client/api.ts b/src/client/api.ts index 31da2197..16530f55 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -198,6 +198,43 @@ export interface BroadcastTransferRequest { */ 'signed_payload': string; } +/** + * + * @export + * @interface BuildStakingOperationRequest + */ +export interface BuildStakingOperationRequest { + /** + * The ID of the blockchain network + * @type {string} + * @memberof BuildStakingOperationRequest + */ + 'network_id': string; + /** + * The ID of the asset being staked + * @type {string} + * @memberof BuildStakingOperationRequest + */ + 'asset_id': string; + /** + * The onchain address from which the staking transaction originates and is responsible for signing the transaction. + * @type {string} + * @memberof BuildStakingOperationRequest + */ + 'address_id': string; + /** + * The type of staking operation + * @type {string} + * @memberof BuildStakingOperationRequest + */ + 'action': string; + /** + * + * @type {{ [key: string]: string; }} + * @memberof BuildStakingOperationRequest + */ + 'options': { [key: string]: string; }; +} /** * * @export @@ -497,6 +534,37 @@ export interface ServerSignerEventList { */ 'total_count': number; } +/** + * + * @export + * @interface ServerSignerList + */ +export interface ServerSignerList { + /** + * + * @type {Array} + * @memberof ServerSignerList + */ + 'data': Array; + /** + * True if this list has another page of items after this one that can be fetched. + * @type {boolean} + * @memberof ServerSignerList + */ + 'has_more': boolean; + /** + * The page token to be used to fetch the next page. + * @type {string} + * @memberof ServerSignerList + */ + 'next_page': string; + /** + * The total number of server-signers for the project. + * @type {number} + * @memberof ServerSignerList + */ + 'total_count': number; +} /** * An event representing a signature creation. * @export @@ -599,6 +667,19 @@ export interface SignatureCreationEventResult { } +/** + * An onchain transaction to help realize a staking action. + * @export + * @interface StakingOperation + */ +export interface StakingOperation { + /** + * + * @type {Transaction} + * @memberof StakingOperation + */ + 'transaction': Transaction; +} /** * A trade of an asset to another asset * @export @@ -1690,10 +1771,12 @@ export const ServerSignersApiAxiosParamCreator = function (configuration?: Confi /** * List server signers for the current project * @summary List server signers for the current project + * @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10. + * @param {string} [page] A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listServerSigners: async (options: RawAxiosRequestConfig = {}): Promise => { + listServerSigners: async (limit?: number, page?: string, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/v1/server_signers`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -1706,6 +1789,14 @@ export const ServerSignersApiAxiosParamCreator = function (configuration?: Confi const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -1847,11 +1938,13 @@ export const ServerSignersApiFp = function(configuration?: Configuration) { /** * List server signers for the current project * @summary List server signers for the current project + * @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10. + * @param {string} [page] A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listServerSigners(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.listServerSigners(options); + async listServerSigners(limit?: number, page?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listServerSigners(limit, page, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['ServerSignersApi.listServerSigners']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -1929,11 +2022,13 @@ export const ServerSignersApiFactory = function (configuration?: Configuration, /** * List server signers for the current project * @summary List server signers for the current project + * @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10. + * @param {string} [page] A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listServerSigners(options?: any): AxiosPromise { - return localVarFp.listServerSigners(options).then((request) => request(axios, basePath)); + listServerSigners(limit?: number, page?: string, options?: any): AxiosPromise { + return localVarFp.listServerSigners(limit, page, options).then((request) => request(axios, basePath)); }, /** * Submit the result of a server signer event @@ -2001,11 +2096,13 @@ export interface ServerSignersApiInterface { /** * List server signers for the current project * @summary List server signers for the current project + * @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10. + * @param {string} [page] A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ServerSignersApiInterface */ - listServerSigners(options?: RawAxiosRequestConfig): AxiosPromise; + listServerSigners(limit?: number, page?: string, options?: RawAxiosRequestConfig): AxiosPromise; /** * Submit the result of a server signer event @@ -2079,12 +2176,14 @@ export class ServerSignersApi extends BaseAPI implements ServerSignersApiInterfa /** * List server signers for the current project * @summary List server signers for the current project + * @param {number} [limit] A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10. + * @param {string} [page] A cursor for pagination across multiple pages of results. Don\'t include this parameter on the first call. Use the next_page value returned in a previous response to request subsequent results. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ServerSignersApi */ - public listServerSigners(options?: RawAxiosRequestConfig) { - return ServerSignersApiFp(this.configuration).listServerSigners(options).then((request) => request(this.axios, this.basePath)); + public listServerSigners(limit?: number, page?: string, options?: RawAxiosRequestConfig) { + return ServerSignersApiFp(this.configuration).listServerSigners(limit, page, options).then((request) => request(this.axios, this.basePath)); } /** @@ -2116,6 +2215,132 @@ export class ServerSignersApi extends BaseAPI implements ServerSignersApiInterfa +/** + * StakeApi - axios parameter creator + * @export + */ +export const StakeApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Build a new staking operation + * @summary Build a new staking operation + * @param {BuildStakingOperationRequest} [buildStakingOperationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + buildStakingOperation: async (buildStakingOperationRequest?: BuildStakingOperationRequest, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/v1/stake/build`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(buildStakingOperationRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * StakeApi - functional programming interface + * @export + */ +export const StakeApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = StakeApiAxiosParamCreator(configuration) + return { + /** + * Build a new staking operation + * @summary Build a new staking operation + * @param {BuildStakingOperationRequest} [buildStakingOperationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async buildStakingOperation(buildStakingOperationRequest?: BuildStakingOperationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.buildStakingOperation(buildStakingOperationRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['StakeApi.buildStakingOperation']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * StakeApi - factory interface + * @export + */ +export const StakeApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = StakeApiFp(configuration) + return { + /** + * Build a new staking operation + * @summary Build a new staking operation + * @param {BuildStakingOperationRequest} [buildStakingOperationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + buildStakingOperation(buildStakingOperationRequest?: BuildStakingOperationRequest, options?: any): AxiosPromise { + return localVarFp.buildStakingOperation(buildStakingOperationRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * StakeApi - interface + * @export + * @interface StakeApi + */ +export interface StakeApiInterface { + /** + * Build a new staking operation + * @summary Build a new staking operation + * @param {BuildStakingOperationRequest} [buildStakingOperationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StakeApiInterface + */ + buildStakingOperation(buildStakingOperationRequest?: BuildStakingOperationRequest, options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * StakeApi - object-oriented interface + * @export + * @class StakeApi + * @extends {BaseAPI} + */ +export class StakeApi extends BaseAPI implements StakeApiInterface { + /** + * Build a new staking operation + * @summary Build a new staking operation + * @param {BuildStakingOperationRequest} [buildStakingOperationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StakeApi + */ + public buildStakingOperation(buildStakingOperationRequest?: BuildStakingOperationRequest, options?: RawAxiosRequestConfig) { + return StakeApiFp(this.configuration).buildStakingOperation(buildStakingOperationRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + /** * TradesApi - axios parameter creator * @export diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index ed16539a..4a6aebba 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -6,6 +6,7 @@ import { TransfersApiFactory, AddressesApiFactory, WalletsApiFactory, + ServerSignersApiFactory, } from "../client"; import { BASE_PATH } from "./../client/base"; import { Configuration } from "./../client/configuration"; @@ -100,6 +101,7 @@ export class Coinbase { Coinbase.apiClients.wallet = WalletsApiFactory(config, basePath, axiosInstance); Coinbase.apiClients.address = AddressesApiFactory(config, basePath, axiosInstance); Coinbase.apiClients.transfer = TransfersApiFactory(config, basePath, axiosInstance); + Coinbase.apiClients.serverSigner = ServerSignersApiFactory(config, basePath, axiosInstance); Coinbase.apiKeyPrivateKey = privateKey; Coinbase.useServerSigner = useServerSigner; } diff --git a/src/coinbase/server_signer.ts b/src/coinbase/server_signer.ts new file mode 100644 index 00000000..d0e37824 --- /dev/null +++ b/src/coinbase/server_signer.ts @@ -0,0 +1,65 @@ +import { Coinbase } from "./coinbase"; +import { ServerSigner as ServerSignerModel } from "../client/api"; + +/** + * A representation of a Server-Signer. Server-Signers are assigned to sign transactions for a Wallet. + */ +export class ServerSigner { + private model: ServerSignerModel; + + /** + * Private constructor to prevent direct instantiation outside of factory method. + * Creates a new ServerSigner instance. + * Do not use this method directly. Instead, use ServerSigner.getDefault(). + * + * @ignore + * @param serverSignerModel - The Server-Signer model. + * @hideconstructor + */ + private constructor(serverSignerModel: ServerSignerModel) { + this.model = serverSignerModel; + } + + /** + * Returns the default Server-Signer for the CDP Project. + * + * @returns The default Server-Signer. + * @throws {APIError} if the API request to list Server-Signers fails. + * @throws {Error} if there is no Server-Signer associated with the CDP Project. + */ + public static async getDefault(): Promise { + const response = await Coinbase.apiClients.serverSigner!.listServerSigners(); + if (response.data.data.length === 0) { + throw new Error("No Server-Signer is associated with the project"); + } + + return new ServerSigner(response.data.data[0]); + } + + /** + * Returns the ID of the Server-Signer. + * + * @returns The Server-Signer ID. + */ + public getId(): string { + return this.model.server_signer_id; + } + + /** + * Returns the IDs of the Wallet's the Server-Signer can sign for. + * + * @returns The Wallet IDs. + */ + public getWallets(): string[] | undefined { + return this.model.wallets; + } + + /** + * Returns a String representation of the Server-Signer. + * + * @returns a String representation of the Server-Signer. + */ + public toString(): string { + return `ServerSigner{id: '${this.getId()}', wallets: '${this.getWallets()}'}`; + } +} diff --git a/src/coinbase/tests/server_signer_test.ts b/src/coinbase/tests/server_signer_test.ts new file mode 100644 index 00000000..355c7aa8 --- /dev/null +++ b/src/coinbase/tests/server_signer_test.ts @@ -0,0 +1,95 @@ +import * as crypto from "crypto"; +import { Coinbase } from "../coinbase"; +import { APIError } from "../api_error"; +import { ServerSigner as ServerSignerModel, ServerSignerList } from "../../client"; +import { serverSignersApiMock, mockReturnValue, mockReturnRejectedValue } from "./utils"; +import { ServerSigner } from "../server_signer"; + +describe("ServerSigner", () => { + let serverSigner: ServerSigner; + const serverSignerId: string = crypto.randomUUID(); + const wallets: string[] = Array.from({ length: 3 }, () => crypto.randomUUID()); + const model: ServerSignerModel = { + server_signer_id: serverSignerId, + wallets: wallets, + }; + const serverSignerList: ServerSignerList = { + data: [model], + total_count: 1, + has_more: false, + next_page: "", + }; + const emptyServerSignerList: ServerSignerList = { + data: [], + total_count: 0, + has_more: false, + next_page: "", + }; + + beforeAll(async () => { + Coinbase.apiClients.serverSigner = serverSignersApiMock; + Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnValue(serverSignerList); + serverSigner = await ServerSigner.getDefault(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe(".getDefault", () => { + describe("when a default Server-Signer exists", () => { + beforeEach(() => { + Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnValue(serverSignerList); + }); + + it("should return the default Server-Signer", async () => { + const defaultServerSigner = await ServerSigner.getDefault(); + expect(defaultServerSigner).toBeInstanceOf(ServerSigner); + expect(defaultServerSigner.getId()).toBe(serverSignerId); + expect(defaultServerSigner.getWallets()).toBe(wallets); + expect(Coinbase.apiClients.serverSigner!.listServerSigners).toHaveBeenCalledTimes(1); + }); + }); + + it("should throw an APIError when the request is unsuccessful", async () => { + Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnRejectedValue( + new APIError("Failed to list Server-Signers"), + ); + await expect(ServerSigner.getDefault()).rejects.toThrow(APIError); + expect(Coinbase.apiClients.serverSigner!.listServerSigners).toHaveBeenCalledTimes(1); + }); + + describe("when a default Server-Signer does not exist", () => { + beforeEach(() => { + Coinbase.apiClients.serverSigner!.listServerSigners = + mockReturnValue(emptyServerSignerList); + }); + + it("should return an error", async () => { + await expect(ServerSigner.getDefault()).rejects.toThrow( + new Error("No Server-Signer is associated with the project"), + ); + }); + }); + }); + + describe("#getId", () => { + it("should return the Server-Signer ID", async () => { + expect(serverSigner.getId()).toBe(serverSignerId); + }); + }); + + describe("#getWallets", () => { + it("should return the list of Wallet IDs", async () => { + expect(serverSigner.getWallets()).toBe(wallets); + }); + }); + + describe("#toString", () => { + it("should return the correct string representation", async () => { + expect(serverSigner.toString()).toBe( + `ServerSigner{id: '${serverSignerId}', wallets: '${wallets}'}`, + ); + }); + }); +}); diff --git a/src/coinbase/tests/utils.ts b/src/coinbase/tests/utils.ts index 4a57fe07..94587e78 100644 --- a/src/coinbase/tests/utils.ts +++ b/src/coinbase/tests/utils.ts @@ -185,3 +185,7 @@ export const transfersApiMock = { getTransfer: jest.fn(), listTransfers: jest.fn(), }; + +export const serverSignersApiMock = { + listServerSigners: jest.fn(), +}; diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index f414ac80..b413810d 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -14,6 +14,7 @@ import { Wallet as WalletModel, Transfer as TransferModel, WalletList, + ServerSignerList, } from "./../client/api"; import { Address } from "./address"; import { Wallet } from "./wallet"; @@ -288,6 +289,26 @@ export type TransferAPIClient = { ): AxiosPromise; }; +/** + * ServerSignerAPI client type definition. + */ +export type ServerSignerAPIClient = { + /** + * Lists Server-Signers. + * + * @param limit - The maximum number of Server-Signers to return. + * @param page - The cursor for pagination across multiple pages of Server-Signers. + * @param options - Axios request options. + * @returns - A promise resolving to the Server-Signer list. + * @throws {APIError} If the request fails. + */ + listServerSigners( + limit?: number, + page?: string, + options?: AxiosRequestConfig, + ): AxiosPromise; +}; + /** * API clients type definition for the Coinbase SDK. * Represents the set of API clients available in the SDK. @@ -297,6 +318,7 @@ export type ApiClients = { wallet?: WalletAPIClient; address?: AddressAPIClient; transfer?: TransferAPIClient; + serverSigner?: ServerSignerAPIClient; }; /**