From a6f0ed7bfa449a854d62fb63510878d0254e50d0 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 8 Mar 2024 13:23:30 +0200 Subject: [PATCH] mahmoud/eng-3688-detecting-the-wallet-api-bitcoin-provider (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * inject webbtc providersArray and export utils * init listen util and update getProvider to return the provider object * init request method * fix provider imports * export request method types * Update Stacks request types * Update types * Add more stx request types * Remove placeholder code * Update Stacks types * init btc methods * Finalize btc request types * Simplify types * re-arrange request types * Add stx getAddress and getAccounts types * Prepend Stx types to avoid naming collisions * fix error type * Set default type for Request * update signPsbt types and added success response type * Fix type inferrence * update rpc success response * updated btc methods types and added jsdocs * Added JSDocs for error types * Construct Request type from request types for each network * Add type for method names * Set return and params to never for unknown methods * Update contract call & deploy method names * Start adding zod schemas * Remove zod * code review fixes * Add exports * remove listen request and update rpcid type * added response type check util * Proposed typing for the result (#75) * Proposed typing for the result * Remove cancel state --------- Co-authored-by: Eduard Bardají Puig Co-authored-by: Victor Kirov --- src/call/index.ts | 25 --- src/call/types.ts | 10 -- src/capabilities/index.ts | 2 +- src/index.ts | 2 +- src/provider/index.ts | 20 ++- src/provider/types.ts | 25 ++- src/request/index.ts | 42 +++++ src/request/types/btcMethods.ts | 125 +++++++++++++++ src/request/types/index.ts | 43 ++++++ src/request/types/stxMethods.ts | 261 ++++++++++++++++++++++++++++++++ src/types.ts | 84 ++++++++++ tsconfig.json | 2 +- 12 files changed, 597 insertions(+), 44 deletions(-) delete mode 100644 src/call/index.ts delete mode 100644 src/call/types.ts create mode 100644 src/request/index.ts create mode 100644 src/request/types/btcMethods.ts create mode 100644 src/request/types/index.ts create mode 100644 src/request/types/stxMethods.ts diff --git a/src/call/index.ts b/src/call/index.ts deleted file mode 100644 index c9469ff..0000000 --- a/src/call/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Json } from 'jsontokens'; -import { createUnsecuredToken } from 'jsontokens'; - -import { getProviderOrThrow } from '../provider'; -import type { CallWalletOptions } from './types'; - -export const callWalletPopup = async (options: CallWalletOptions) => { - const provider = await getProviderOrThrow(options.getProvider); - - const { method } = options.payload; - if (!method) { - throw new Error('A wallet method is required'); - } - - const request = createUnsecuredToken(options.payload as unknown as Json); - try { - const response = await provider.call(request); - options.onFinish?.(response); - } catch (error) { - console.error('[Connect] Error during call request', error); - options.onCancel?.(); - } -}; - -export * from './types'; diff --git a/src/call/types.ts b/src/call/types.ts deleted file mode 100644 index ba82bca..0000000 --- a/src/call/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RequestOptions, RequestPayload } from '../types'; - -export interface CallWalletPayload extends RequestPayload { - method: string; - params?: any[]; -} - -export type CallWalletResponse = Record; - -export type CallWalletOptions = RequestOptions; diff --git a/src/capabilities/index.ts b/src/capabilities/index.ts index af4f10f..cc2f54c 100644 --- a/src/capabilities/index.ts +++ b/src/capabilities/index.ts @@ -28,7 +28,7 @@ const extractOrValidateCapabilities = ( }; const capabilityMap: CapabilityMap = { - call: validateCapability('call'), + request: validateCapability('request'), connect: validateCapability('connect'), signMessage: validateCapability('signMessage'), signTransaction: validateCapability('signTransaction'), diff --git a/src/index.ts b/src/index.ts index b19f8d6..c682823 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './addresses'; -export * from './call'; +export * from './request'; export * from './capabilities'; export * from './inscriptions'; export * from './messages'; diff --git a/src/provider/index.ts b/src/provider/index.ts index 6530f3c..e26bd41 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -1,9 +1,10 @@ -import type { BitcoinProvider } from './types'; +import { type BitcoinProvider, type WebbtcProvider } from './types'; export async function getProviderOrThrow( getProvider?: () => Promise ): Promise { - const provider = (await getProvider?.()) || window.XverseProviders?.BitcoinProvider || window.BitcoinProvider; + const provider = + (await getProvider?.()) || window.XverseProviders?.BitcoinProvider || window.BitcoinProvider; if (!provider) { throw new Error('No Bitcoin wallet installed'); @@ -12,4 +13,19 @@ export async function getProviderOrThrow( return provider; } +export function getProviders(): WebbtcProvider[] { + if (!window.webbtc_providers) window.webbtc_providers = []; + return window.webbtc_providers; +} + +export function getProviderById(providerId: string) { + if (Array.isArray(window.webbtc_providers)) { + const provider = window.webbtc_providers.find((provider) => provider.id === providerId); + return provider?.id?.split('.').reduce((acc: any, part) => acc?.[part], window); + } else { + console.log('window.webbtc_providers is not defined or not an array'); + return undefined; + } +} + export * from './types'; diff --git a/src/provider/types.ts b/src/provider/types.ts index a0eccd9..14d0790 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -1,5 +1,5 @@ +import { Params, Requests } from '../request'; import type { GetAddressResponse } from '../addresses'; -import type { CallWalletResponse } from '../call'; import type { GetCapabilitiesResponse } from '../capabilities'; import type { CreateInscriptionResponse, CreateRepeatInscriptionsResponse } from '../inscriptions'; import type { SignMessageResponse } from '../messages'; @@ -8,9 +8,14 @@ import type { SignMultipleTransactionsResponse, SignTransactionResponse, } from '../transactions'; +import { RpcResponse } from '../types'; interface BaseBitcoinProvider { - call: (request: string) => Promise; + request: ( + method: Method, + options: Params, + providerId?: string + ) => Promise>; connect: (request: string) => Promise; signMessage: (request: string) => Promise; signTransaction: (request: string) => Promise; @@ -26,13 +31,25 @@ export interface BitcoinProvider extends BaseBitcoinProvider { getCapabilities?: (request: string) => Promise; } +export interface WebbtcProvider { + id: string; + name: string; + icon: string; + webUrl?: string; + chromeWebStoreUrl?: string; + mozillaAddOnsUrl?: string; + googlePlayStoreUrl?: string; + iOSAppStoreUrl?: string; + methods?: string[]; +} + declare global { interface XverseProviders { BitcoinProvider?: BitcoinProvider; } - interface Window { BitcoinProvider?: BitcoinProvider; - XverseProviders?: XverseProviders + XverseProviders?: XverseProviders; + webbtc_providers?: WebbtcProvider[]; } } diff --git a/src/request/index.ts b/src/request/index.ts new file mode 100644 index 0000000..c901dd6 --- /dev/null +++ b/src/request/index.ts @@ -0,0 +1,42 @@ +import { getProviderById } from '../provider'; +import { RpcBase, RpcResult, RpcSuccessResponse } from '../types'; +import { Params, Requests } from './types'; + +export const request = async ( + method: Method, + params: Params, + providerId?: string +): Promise> => { + let provider = window.XverseProviders?.BitcoinProvider || window.BitcoinProvider; + if (providerId) { + provider = await getProviderById(providerId); + } + if (!provider) { + throw new Error('no wallet provider was found'); + } + if (!method) { + throw new Error('A wallet method is required'); + } + + const response = await provider.request(method, params); + + if (isRpcSuccessResponse(response)) { + return { + status: 'success', + result: response.result, + }; + } + + return { + status: 'error', + error: response.error, + }; +}; + +const isRpcSuccessResponse = ( + response: RpcBase +): response is RpcSuccessResponse => { + return Object.hasOwn(response, 'result') && !!(response as RpcSuccessResponse).result; +}; + +export * from './types'; diff --git a/src/request/types/btcMethods.ts b/src/request/types/btcMethods.ts new file mode 100644 index 0000000..092cdf0 --- /dev/null +++ b/src/request/types/btcMethods.ts @@ -0,0 +1,125 @@ +/** + * Represents the types and interfaces related to BTC methods. + */ + +import { Address, AddressPurpose } from '../../addresses'; +import { MethodParamsAndResult } from '../../types'; + +type GetInfoResult = { + version: number | string; + methods?: Array; + supports?: Array; +}; + +export type GetInfo = MethodParamsAndResult; + +type GetAddressesParams = { + /** + * The purposes for which to generate addresses. + * possible values are "payment", "ordinals", ... + */ + purposes: Array; + /** + * a message to be displayed to the user in the request prompt. + */ + message?: string; +}; + +/** + * The addresses generated for the given purposes. + */ +type GetAddressesResult = { + addresses: Array
; +}; + +export type GetAddresses = MethodParamsAndResult; + +type SignMessageParams = { + /** + * The address used for signing. + **/ + address: string; + /** + * The message to sign. + **/ + message: string; +}; + +type SignMessageResult = { + /** + * The signature of the message. + */ + signature: string; + /** + * hash of the message. + */ + messageHash: string; + /** + * The address used for signing. + */ + address: string; +}; + +export type SignMessage = MethodParamsAndResult; + +type Recipient = { + /** + * The recipient's address. + **/ + address: string; + /** + * The amount to send to the recipient in satoshis. + */ + amount: number; +}; + +type SendTransferParams = { + /** + * Array of recipients to send to. + * The amount to send to each recipient is in satoshis. + */ + recipients: Array; +}; +type SendTransferResult = { + /** + * The transaction id as a hex-encoded string. + */ + txid: string; +}; + +export type SendTransfer = MethodParamsAndResult; + +export type SignPsbtParams = { + /** + * The base64 encoded PSBT to sign. + */ + psbt: string; + /** + * The inputs to sign. + * The key is the address and the value is an array of indexes of the inputs to sign. + */ + signInputs: Record; + /** + * the sigHash type to use for signing. + * will default to the sighash type of the input if not provided. + **/ + allowedSignHash?: number; + /** + * Whether to broadcast the transaction after signing. + **/ + broadcast?: boolean; +}; + +export type SignPsbtResult = { + /** + * The base64 encoded PSBT after signing. + */ + psbt: string; + /** + * The transaction id as a hex-encoded string. + * This is only returned if the transaction was broadcast. + **/ + txid?: string; +}; + +export type SignPsbt = MethodParamsAndResult; diff --git a/src/request/types/index.ts b/src/request/types/index.ts new file mode 100644 index 0000000..0610fd2 --- /dev/null +++ b/src/request/types/index.ts @@ -0,0 +1,43 @@ +import { RpcSuccessResponse } from 'src/types'; +import { GetAddresses, GetInfo, SendTransfer, SignMessage, SignPsbt } from './btcMethods'; +import { + StxCallContract, + StxDeployContract, + StxGetAccounts, + StxGetAddresses, + StxSignStructuredMessage, + StxSignStxMessage, + StxSignTransaction, + StxTransferStx, +} from './stxMethods'; + +export interface StxRequests { + stx_callContract: StxCallContract; + stx_deployContract: StxDeployContract; + stx_getAccounts: StxGetAccounts; + stx_getAddresses: StxGetAddresses; + stx_signMessage: StxSignStxMessage; + stx_signStructuredMessage: StxSignStructuredMessage; + stx_signTransaction: StxSignTransaction; + stx_transferStx: StxTransferStx; +} + +export type StxRequestMethod = keyof StxRequests; + +export interface BtcRequests { + getInfo: GetInfo; + getAddresses: GetAddresses; + signMessage: SignMessage; + sendTransfer: SendTransfer; + signPsbt: SignPsbt; +} + +export type BtcRequestMethod = keyof BtcRequests; + +export type Requests = BtcRequests & StxRequests; + +export type Return = Method extends keyof Requests ? Requests[Method]['result'] : never; +export type Params = Method extends keyof Requests ? Requests[Method]['params'] : never; + +export * from './stxMethods'; +export * from './btcMethods'; diff --git a/src/request/types/stxMethods.ts b/src/request/types/stxMethods.ts new file mode 100644 index 0000000..6a47dda --- /dev/null +++ b/src/request/types/stxMethods.ts @@ -0,0 +1,261 @@ +import { MethodParamsAndResult } from '../../types'; + +interface Pubkey { + /** + * When sending a transfer STX request to a wallet, users can generally + * choose from which accout they want to send the STX tokens from. In + * cases where applications want the transfer to be made from a specific + * account, they can provide the `pubkey` of the address they'd like the + * transfer to be made from. It is up to wallet providers to handle this + * field as they see fit. + */ + pubkey: string; +} + +interface Address { + /** + * A Crockford base-32 encoded Stacks address. + */ + address: string; +} + +interface ContractName { + /** + * The name of the contract. + */ + contract: string; +} + +interface PostConditions { + /** + * A hex-encoded string representing the post conditions. + * + * A post condition may be converted to it's hex representation using the `serializePostCondition` helper from the `@stacks/transactions` package, + * + * ```js + * import { serializePostCondition } from '@stacks/transactions'; + * + * const postCondition = somePostCondition; + * const hexPostCondition = serializePostCondition(postCondition).toString('hex'); + * ``` + */ + postConditions: Array; +} + +interface PostConditionMode { + /** + * The mode of the post conditions. + */ + postConditionMode: number; +} + +interface AnchorMode { + /** + * The anchor mode. + */ + anchorMode: 'TODO'; // AnchorMode +} + +interface Nonce { + /** + * A number in string format. + */ + nonce: string; // BigInt +} + +interface ParameterFormatVersion { + /** + * Version of parameter format. + */ + version: string; +} + +interface Sponsored { + /** + * Whether the transaction is sponsored. + */ + sponsored: boolean; +} + +interface Recipient { + /** + * The recipeint's Crockford base-32 encoded Stacks address. + */ + recipient: string; +} + +interface Amount { + /** + * Amount of STX tokens to transfer in microstacks as a string. Anything + * parseable by `BigInt` is acceptable. + * + * Example, + * + * ```js + * const amount1 = 1234; + * const amount2 = 1234n; + * const amount3 = '1234'; + * ``` + */ + amount: number | string; +} + +interface Memo { + /** + * A string representing the memo. + */ + memo: string; +} + +interface TxId { + /** + * The transaction ID of the transfer STX transaction as a hex-encoded string. + */ + txid: string; +} + +interface Transaction { + /** + * An STX transaction as a hex-encoded string. + */ + transaction: string; +} + +interface Message { + /** + * Message payload to be signed. + */ + message: string; +} + +interface Signature { + /** + * Signature of the message. + */ + signature: string; +} + +interface PublicKey { + /** + * Public key as hex-encoded string. + */ + publicKey: string; +} + +interface Domain { + /** + * The domain to be signed. + */ + domain: string; +} + +interface CodeBody { + /** + * The code body of the Clarity contract. + */ + codeBody: string; +} + +interface ClarityVersion { + /** + * The Clarity version of the contract. + */ + clarityVersion?: string; +} + +// Types for `stx_callContract` request +export interface CallContractParams { + /** + * The contract's Crockford base-32 encoded Stacks address and name. + * + * E.g. `"SPKE...GD5C.my-contract"` + */ + contract: string; + + /** + * The name of the function to call. + * + * Note: spec changes ongoing, + * https://github.com/stacksgov/sips/pull/166#pullrequestreview-1914236999 + */ + functionName: string; + + /** + * The function's arguments. The arguments are expected to be hex-encoded + * strings of Clarity values. + * + * To convert Clarity values to their hex representation, the `cvToString` + * helper from the `@stacks/transactions` package may be helpful. + * + * ```js + * import { cvToString } from '@stacks/transactions'; + * + * const functionArgs = [someClarityValue1, someClarityValue2]; + * const hexArgs = functionArgs.map(cvToString); + * ``` + */ + arguments?: Array; +} +type CallContractResult = TxId & Transaction; +export type StxCallContract = MethodParamsAndResult; + +// Types for `stx_transferStx` request +type TransferStxParams = Amount & + Recipient & + Partial & + Partial & + Partial & + Partial & + Partial; +type TransferStxResult = TxId & Transaction; +export type StxTransferStx = MethodParamsAndResult; + +// Types for `stx_signMessage` request +type SignStxMessageParams = Message & Partial & Partial; +type SignStxMessageResult = Signature & PublicKey; +export type StxSignStxMessage = MethodParamsAndResult; + +// Types for `stx_signStructuredMessage` request +type SignStructuredMessageParams = Domain & + Message & + Partial & + Partial; +type SignStructuredMessageResult = Signature & PublicKey; +export type StxSignStructuredMessage = MethodParamsAndResult< + SignStructuredMessageParams, + SignStructuredMessageResult +>; + +// Types for `stx_deployContract` request +type DeployContractParams = CodeBody & + ContractName & + Sponsored & + Partial & + Partial & + Partial & + Partial & + Partial; +type DeployContractResult = TxId & Transaction; +export type StxDeployContract = MethodParamsAndResult; + +// Types for `stx_getAccounts` request +type GetAccountsParams = {}; +type GetAccountsResult = { + addresses: Array
; +}; +export type StxGetAccounts = MethodParamsAndResult; + +// Types for `stx_getAddresses` request +type GetAddressesParams = {}; +type GetAddressesResult = { + addresses: Array
; +}; +export type StxGetAddresses = MethodParamsAndResult; + +// Types for `stx_signTransaction` request +export type SignTransactionParams = Transaction & Partial; +export type SignTransactionResult = Transaction; +export type StxSignTransaction = MethodParamsAndResult< + SignTransactionParams, + SignTransactionResult +>; diff --git a/src/types.ts b/src/types.ts index 2b68fde..1b6e0d2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { BitcoinProvider } from './provider'; +import { Requests, Return } from './request'; export enum BitcoinNetworkType { Mainnet = 'Mainnet', @@ -20,3 +21,86 @@ export interface RequestOptions { payload: Payload; getProvider?: () => Promise; } + +// RPC Request and Response types + +export type RpcId = string | null; + +export interface RpcBase { + jsonrpc: '2.0'; + id: RpcId; +} +export interface RpcRequest extends RpcBase { + method: T; + params: U; +} + +export interface MethodParamsAndResult { + params: TParams; + result: TResult; +} + +/** + * @enum {number} RpcErrorCode + * @description JSON-RPC error codes + * @see https://www.jsonrpc.org/specification#error_object + */ +export enum RpcErrorCode { + /** + * Parse error Invalid JSON + **/ + PARSE_ERROR = -32700, + /** + * The JSON sent is not a valid Request object. + **/ + INVALID_REQUEST = -32600, + /** + * The method does not exist/is not available. + **/ + METHOD_NOT_FOUND = -32601, + /** + * Invalid method parameter(s). + */ + INVALID_PARAMS = -32602, + /** + * Internal JSON-RPC error. + * This is a generic error, used when the server encounters an error in performing the request. + **/ + INTERNAL_ERROR = -32603, + /** + * user rejected/canceled the request + */ + USER_REJECTION = -32000, + /** + * method is not supported for the address provided + */ + METHOD_NOT_SUPPORTED = -32001, +} + +export interface RpcError { + code: number | RpcErrorCode; + message: string; + data?: any; +} + +export interface RpcErrorResponse extends RpcBase { + error: TError; +} + +export interface RpcSuccessResponse extends RpcBase { + result: Return; +} + +export type RpcResponse = + | RpcSuccessResponse + | RpcErrorResponse; + +export type RpcResult = + | { + result: RpcSuccessResponse['result']; + status: 'success'; + } + | { + error: RpcErrorResponse['error']; + status: 'error'; + }; diff --git a/tsconfig.json b/tsconfig.json index 2624399..3cc25ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "incremental": true, - "target": "es2020", + "target": "ES2022", "module": "commonjs", "moduleResolution": "node", "baseUrl": ".",