diff --git a/.gitignore b/.gitignore index 490f098..737d98a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage ts-adv-trade-test-private.ts examples/ts-app-priv.ts examples/ts-commerce.ts +ts-exchange-priv.ts diff --git a/src/CBAppClient.ts b/src/CBAppClient.ts index f6fe4c4..92b24ba 100644 --- a/src/CBAppClient.ts +++ b/src/CBAppClient.ts @@ -34,6 +34,8 @@ export class CBAppClient extends BaseRestClient { super(restClientOptions, { ...requestOptions, headers: { + // Some endpoints return a warning if a version header isn't included: https://docs.cdp.coinbase.com/coinbase-app/docs/versioning + // Currently set to a date from the changelog: https://docs.cdp.coinbase.com/coinbase-app/docs/changelog 'CB-VERSION': '2024-09-13', ...requestOptions.headers, }, diff --git a/src/lib/BaseRestClient.ts b/src/lib/BaseRestClient.ts index 11faebe..64579f3 100644 --- a/src/lib/BaseRestClient.ts +++ b/src/lib/BaseRestClient.ts @@ -351,8 +351,9 @@ export abstract class BaseRestClient { requestOptions: { ...this.options, // Prevent credentials from leaking into error messages - apiKeyName: 'omittedFromError', - apiPrivateKey: 'omittedFromError', + apiKey: 'omittedFromError', + apiSecret: 'omittedFromError', + apiPassphrase: 'omittedFromError', cdpApiKey: 'omittedFromError', }, requestParams, @@ -385,6 +386,7 @@ export abstract class BaseRestClient { const apiKey = this.apiKey; const apiSecret = this.apiSecret; + const apiPassphrase = this.apiPassphrase; const jwtExpiresSeconds = this.options.jwtExpiresSeconds || 120; if (!apiKey) { @@ -448,9 +450,12 @@ export abstract class BaseRestClient { // See: https://github.com/tiagosiebler/coinbase-api/issues/24 } - case REST_CLIENT_TYPE_ENUM.exchange: { - // Docs: https://docs.cdp.coinbase.com/exchange/docs/rest-auth - const timestampInSeconds = timestampInMs / 1000; + // Docs: https://docs.cdp.coinbase.com/exchange/docs/rest-auth + case REST_CLIENT_TYPE_ENUM.exchange: + + // Docs: https://docs.cdp.coinbase.com/intx/docs/rest-auth + case REST_CLIENT_TYPE_ENUM.international: { + const timestampInSeconds = timestampInMs / 1000; // decimals are OK const signInput = timestampInSeconds + method + endpoint + requestBodyString; @@ -459,7 +464,7 @@ export abstract class BaseRestClient { throw new Error(`No API secret provided, cannot sign request.`); } - if (!this.apiPassphrase) { + if (!apiPassphrase) { throw new Error(`No API passphrase provided, cannot sign request.`); } @@ -468,13 +473,14 @@ export abstract class BaseRestClient { apiSecret, 'base64', 'SHA-256', + 'base64:web', ); const headers = { + 'CB-ACCESS-KEY': apiKey, 'CB-ACCESS-SIGN': sign, 'CB-ACCESS-TIMESTAMP': timestampInSeconds, - 'CB-ACCESS-PASSPHRASE': this.apiPassphrase, - 'CB-ACCESS-KEY': apiKey, + 'CB-ACCESS-PASSPHRASE': apiPassphrase, }; return { @@ -486,16 +492,16 @@ export abstract class BaseRestClient { }, }; - // TODO: is there demand for FIX + // For CB Exchange, is there demand for FIX // Docs, FIX: https://docs.cdp.coinbase.com/exchange/docs/fix-connectivity - } - // Docs: https://docs.cdp.coinbase.com/intx/docs/rest-auth - case REST_CLIENT_TYPE_ENUM.international: + // For CB International, is there demand for FIX + // Docs, FIX: https://docs.cdp.coinbase.com/intx/docs/fix-overview + } // Docs: https://docs.cdp.coinbase.com/prime/docs/rest-authentication case REST_CLIENT_TYPE_ENUM.prime: { - const timestampInSeconds = String(Math.floor(timestampInMs / 1000)); + const timestampInSeconds = Math.floor(timestampInMs / 1000); // decimal not allowed const signInput = timestampInSeconds + method + endpoint + requestBodyString; @@ -504,7 +510,7 @@ export abstract class BaseRestClient { throw new Error(`No API secret provided, cannot sign request.`); } - if (!this.apiPassphrase) { + if (!apiPassphrase) { throw new Error(`No API passphrase provided, cannot sign request.`); } @@ -513,15 +519,19 @@ export abstract class BaseRestClient { apiSecret, 'base64', 'SHA-256', + 'base64:web', ); const headers = { - 'CB-ACCESS-TIMESTAMP': timestampInSeconds, - 'CB-ACCESS-SIGN': sign, - 'CB-ACCESS-PASSPHRASE': this.apiPassphrase, - 'CB-ACCESS-KEY': apiKey, + 'X-CB-ACCESS-KEY': apiKey, + 'X-CB-ACCESS-PASSPHRASE': apiPassphrase, + 'X-CB-ACCESS-SIGNATURE': sign, + 'X-CB-ACCESS-TIMESTAMP': timestampInSeconds, }; + // For CB Prime, is there demand for FIX + // Docs, FIX: https://docs.cdp.coinbase.com/prime/docs/fix-connectivity + return { ...res, sign: sign, @@ -530,12 +540,6 @@ export abstract class BaseRestClient { ...headers, }, }; - - // For CB International, is there demand for FIX - // Docs, FIX: https://docs.cdp.coinbase.com/intx/docs/fix-overview - - // For CB Prime, is there demand for FIX - // Docs, FIX: https://docs.cdp.coinbase.com/prime/docs/fix-connectivity } case REST_CLIENT_TYPE_ENUM.commerce: { // https://docs.cdp.coinbase.com/commerce-onchain/docs/getting-started diff --git a/src/lib/webCryptoAPI.ts b/src/lib/webCryptoAPI.ts index 0d9272b..9c8ff5e 100644 --- a/src/lib/webCryptoAPI.ts +++ b/src/lib/webCryptoAPI.ts @@ -10,6 +10,17 @@ function bufferToB64(buffer: ArrayBuffer): string { return globalThis.btoa(binary); } +function b64StringToBuffer(input: string): ArrayBuffer { + const binaryString = atob(input); // Decode base64 string + const buffer = new Uint8Array(binaryString.length); + + // Convert binary string to a Uint8Array + for (let i = 0; i < binaryString.length; i++) { + buffer[i] = binaryString.charCodeAt(i); + } + return buffer; +} + export type SignEncodeMethod = 'hex' | 'base64'; export type SignAlgorithm = 'SHA-256' | 'SHA-512'; @@ -51,12 +62,33 @@ export async function signMessage( secret: string, method: SignEncodeMethod, algorithm: SignAlgorithm, + secretEncodeMethod: 'base64:web' | 'utf', ): Promise { const encoder = new TextEncoder(); + let encodedSecret; + + switch (secretEncodeMethod) { + // case 'base64:node': { + // encodedSecret = Buffer.from(secret, 'base64'); + // break; + // } + case 'base64:web': { + encodedSecret = b64StringToBuffer(secret); + break; + } + case 'utf': { + encodedSecret = encoder.encode(secret); + break; + } + default: { + throw new Error(`Unhandled encoding: "${secretEncodeMethod}"`); + } + } + const key = await globalThis.crypto.subtle.importKey( 'raw', - encoder.encode(secret), + encodedSecret, { name: 'HMAC', hash: algorithm }, false, ['sign'],