├── .eslintrc.json ├── .github ├── pull_request_template.md └── workflows │ ├── e2e_test.yml │ ├── lint.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── SECURITY.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── client_api.AddressesApi.html │ ├── client_api.AssetsApi.html │ ├── client_api.BalanceHistoryApi.html │ ├── client_api.ContractEventsApi.html │ ├── client_api.ContractInvocationsApi.html │ ├── client_api.ExternalAddressesApi.html │ ├── client_api.FundApi.html │ ├── client_api.MPCWalletStakeApi.html │ ├── client_api.NetworksApi.html │ ├── client_api.OnchainIdentityApi.html │ ├── client_api.ReputationApi.html │ ├── client_api.ServerSignersApi.html │ ├── client_api.SmartContractsApi.html │ ├── client_api.SmartWalletsApi.html │ ├── client_api.StakeApi.html │ ├── client_api.TradesApi.html │ ├── client_api.TransactionHistoryApi.html │ ├── client_api.TransfersApi.html │ ├── client_api.UsersApi.html │ ├── client_api.WalletsApi.html │ ├── client_api.WebhooksApi.html │ ├── client_base.BaseAPI.html │ ├── client_base.RequiredError.html │ ├── client_configuration.Configuration.html │ ├── coinbase_address.Address.html │ ├── coinbase_address_external_address.ExternalAddress.html │ ├── coinbase_address_reputation.AddressReputation.html │ ├── coinbase_address_wallet_address.WalletAddress.html │ ├── coinbase_api_error.APIError.html │ ├── coinbase_api_error.AlreadyExistsError.html │ ├── coinbase_api_error.FaucetLimitReachedError.html │ ├── coinbase_api_error.InternalError.html │ ├── coinbase_api_error.InvalidAddressError.html │ ├── coinbase_api_error.InvalidAddressIDError.html │ ├── coinbase_api_error.InvalidAmountError.html │ ├── coinbase_api_error.InvalidAssetIDError.html │ ├── coinbase_api_error.InvalidDestinationError.html │ ├── coinbase_api_error.InvalidLimitError.html │ ├── coinbase_api_error.InvalidNetworkIDError.html │ ├── coinbase_api_error.InvalidPageError.html │ ├── coinbase_api_error.InvalidSignedPayloadError.html │ ├── coinbase_api_error.InvalidTransferIDError.html │ ├── coinbase_api_error.InvalidTransferStatusError.html │ ├── coinbase_api_error.InvalidWalletError.html │ ├── coinbase_api_error.InvalidWalletIDError.html │ ├── coinbase_api_error.MalformedRequestError.html │ ├── coinbase_api_error.NetworkFeatureUnsupportedError.html │ ├── coinbase_api_error.NotFoundError.html │ ├── coinbase_api_error.ResourceExhaustedError.html │ ├── coinbase_api_error.UnauthorizedError.html │ ├── coinbase_api_error.UnimplementedError.html │ ├── coinbase_api_error.UnsupportedAssetError.html │ ├── coinbase_asset.Asset.html │ ├── coinbase_authenticator.CoinbaseAuthenticator.html │ ├── coinbase_balance.Balance.html │ ├── coinbase_balance_map.BalanceMap.html │ ├── coinbase_coinbase.Coinbase.html │ ├── coinbase_contract_event.ContractEvent.html │ ├── coinbase_contract_invocation.ContractInvocation.html │ ├── coinbase_crypto_amount.CryptoAmount.html │ ├── coinbase_errors.AlreadySignedError.html │ ├── coinbase_errors.ArgumentError.html │ ├── coinbase_errors.InvalidAPIKeyFormatError.html │ ├── coinbase_errors.InvalidConfigurationError.html │ ├── coinbase_errors.InvalidUnsignedPayloadError.html │ ├── coinbase_errors.NotSignedError.html │ ├── coinbase_errors.TimeoutError.html │ ├── coinbase_errors.UninitializedSDKError.html │ ├── coinbase_faucet_transaction.FaucetTransaction.html │ ├── coinbase_fiat_amount.FiatAmount.html │ ├── coinbase_fund_operation.FundOperation.html │ ├── coinbase_fund_quote.FundQuote.html │ ├── coinbase_historical_balance.HistoricalBalance.html │ ├── coinbase_payload_signature.PayloadSignature.html │ ├── coinbase_server_signer.ServerSigner.html │ ├── coinbase_smart_contract.SmartContract.html │ ├── coinbase_sponsored_send.SponsoredSend.html │ ├── coinbase_staking_balance.StakingBalance.html │ ├── coinbase_staking_operation.StakingOperation.html │ ├── coinbase_staking_reward.StakingReward.html │ ├── coinbase_trade.Trade.html │ ├── coinbase_transaction.Transaction.html │ ├── coinbase_transfer.Transfer.html │ ├── coinbase_validator.Validator.html │ ├── coinbase_wallet.Wallet.html │ └── coinbase_webhook.Webhook.html ├── enums │ ├── client_api.NetworkIdentifier.html │ ├── client_api.SmartContractType.html │ ├── client_api.StakingRewardFormat.html │ ├── client_api.TokenTransferType.html │ ├── client_api.TransactionType.html │ ├── client_api.ValidatorStatus.html │ ├── client_api.WebhookEventType.html │ ├── client_api.WebhookStatus.html │ ├── coinbase_types.FundOperationStatus.html │ ├── coinbase_types.PayloadSignatureStatus.html │ ├── coinbase_types.ServerSignerStatus.html │ ├── coinbase_types.SmartContractType.html │ ├── coinbase_types.SponsoredSendStatus.html │ ├── coinbase_types.StakeOptionsMode.html │ ├── coinbase_types.StakingRewardFormat.html │ ├── coinbase_types.TransactionStatus.html │ ├── coinbase_types.TransferStatus.html │ └── coinbase_types.ValidatorStatus.html ├── functions │ ├── actions_sendUserOperation.sendUserOperation.html │ ├── actions_waitForUserOperation.waitForUserOperation.html │ ├── client_api.AddressesApiAxiosParamCreator.html │ ├── client_api.AddressesApiFactory.html │ ├── client_api.AddressesApiFp.html │ ├── client_api.AssetsApiAxiosParamCreator.html │ ├── client_api.AssetsApiFactory.html │ ├── client_api.AssetsApiFp.html │ ├── client_api.BalanceHistoryApiAxiosParamCreator.html │ ├── client_api.BalanceHistoryApiFactory.html │ ├── client_api.BalanceHistoryApiFp.html │ ├── client_api.ContractEventsApiAxiosParamCreator.html │ ├── client_api.ContractEventsApiFactory.html │ ├── client_api.ContractEventsApiFp.html │ ├── client_api.ContractInvocationsApiAxiosParamCreator.html │ ├── client_api.ContractInvocationsApiFactory.html │ ├── client_api.ContractInvocationsApiFp.html │ ├── client_api.ExternalAddressesApiAxiosParamCreator.html │ ├── client_api.ExternalAddressesApiFactory.html │ ├── client_api.ExternalAddressesApiFp.html │ ├── client_api.FundApiAxiosParamCreator.html │ ├── client_api.FundApiFactory.html │ ├── client_api.FundApiFp.html │ ├── client_api.MPCWalletStakeApiAxiosParamCreator.html │ ├── client_api.MPCWalletStakeApiFactory.html │ ├── client_api.MPCWalletStakeApiFp.html │ ├── client_api.NetworksApiAxiosParamCreator.html │ ├── client_api.NetworksApiFactory.html │ ├── client_api.NetworksApiFp.html │ ├── client_api.OnchainIdentityApiAxiosParamCreator.html │ ├── client_api.OnchainIdentityApiFactory.html │ ├── client_api.OnchainIdentityApiFp.html │ ├── client_api.ReputationApiAxiosParamCreator.html │ ├── client_api.ReputationApiFactory.html │ ├── client_api.ReputationApiFp.html │ ├── client_api.ServerSignersApiAxiosParamCreator.html │ ├── client_api.ServerSignersApiFactory.html │ ├── client_api.ServerSignersApiFp.html │ ├── client_api.SmartContractsApiAxiosParamCreator.html │ ├── client_api.SmartContractsApiFactory.html │ ├── client_api.SmartContractsApiFp.html │ ├── client_api.SmartWalletsApiAxiosParamCreator.html │ ├── client_api.SmartWalletsApiFactory.html │ ├── client_api.SmartWalletsApiFp.html │ ├── client_api.StakeApiAxiosParamCreator.html │ ├── client_api.StakeApiFactory.html │ ├── client_api.StakeApiFp.html │ ├── client_api.TradesApiAxiosParamCreator.html │ ├── client_api.TradesApiFactory.html │ ├── client_api.TradesApiFp.html │ ├── client_api.TransactionHistoryApiAxiosParamCreator.html │ ├── client_api.TransactionHistoryApiFactory.html │ ├── client_api.TransactionHistoryApiFp.html │ ├── client_api.TransfersApiAxiosParamCreator.html │ ├── client_api.TransfersApiFactory.html │ ├── client_api.TransfersApiFp.html │ ├── client_api.UsersApiAxiosParamCreator.html │ ├── client_api.UsersApiFactory.html │ ├── client_api.UsersApiFp.html │ ├── client_api.WalletsApiAxiosParamCreator.html │ ├── client_api.WalletsApiFactory.html │ ├── client_api.WalletsApiFp.html │ ├── client_api.WebhooksApiAxiosParamCreator.html │ ├── client_api.WebhooksApiFactory.html │ ├── client_api.WebhooksApiFp.html │ ├── client_common.assertParamExists.html │ ├── client_common.createRequestFunction.html │ ├── client_common.serializeDataIfNeeded.html │ ├── client_common.setApiKeyToObject.html │ ├── client_common.setBasicAuthToObject.html │ ├── client_common.setBearerAuthToObject.html │ ├── client_common.setOAuthToObject.html │ ├── client_common.setSearchParams.html │ ├── client_common.toPathString.html │ ├── coinbase_hash.hashMessage.html │ ├── coinbase_hash.hashTypedDataMessage.html │ ├── coinbase_read_contract.readContract.html │ ├── coinbase_types.isMnemonicSeedPhrase.html │ ├── coinbase_types.isWalletData.html │ ├── coinbase_utils.convertStringToHex.html │ ├── coinbase_utils.delay.html │ ├── coinbase_utils.formatDate.html │ ├── coinbase_utils.getWeekBackDate.html │ ├── coinbase_utils.logApiResponse.html │ ├── coinbase_utils.parseUnsignedPayload.html │ ├── coinbase_utils.registerAxiosInterceptors.html │ ├── utils_chain.createNetwork.html │ ├── utils_wait.wait.html │ ├── wallets_createSmartWallet.createSmartWallet.html │ └── wallets_toSmartWallet.toSmartWallet.html ├── hierarchy.html ├── index.html ├── interfaces │ ├── client_api.Address.html │ ├── client_api.AddressBalanceList.html │ ├── client_api.AddressHistoricalBalanceList.html │ ├── client_api.AddressList.html │ ├── client_api.AddressReputation.html │ ├── client_api.AddressReputationMetadata.html │ ├── client_api.AddressTransactionList.html │ ├── client_api.AddressesApiInterface.html │ ├── client_api.Asset.html │ ├── client_api.AssetsApiInterface.html │ ├── client_api.Balance.html │ ├── client_api.BalanceHistoryApiInterface.html │ ├── client_api.BroadcastContractInvocationRequest.html │ ├── client_api.BroadcastExternalTransaction200Response.html │ ├── client_api.BroadcastExternalTransactionRequest.html │ ├── client_api.BroadcastExternalTransferRequest.html │ ├── client_api.BroadcastStakingOperationRequest.html │ ├── client_api.BroadcastTradeRequest.html │ ├── client_api.BroadcastTransferRequest.html │ ├── client_api.BroadcastUserOperationRequest.html │ ├── client_api.BuildStakingOperationRequest.html │ ├── client_api.Call.html │ ├── client_api.CompileSmartContractRequest.html │ ├── client_api.CompiledSmartContract.html │ ├── client_api.ContractEvent.html │ ├── client_api.ContractEventList.html │ ├── client_api.ContractEventsApiInterface.html │ ├── client_api.ContractInvocation.html │ ├── client_api.ContractInvocationList.html │ ├── client_api.ContractInvocationsApiInterface.html │ ├── client_api.CreateAddressRequest.html │ ├── client_api.CreateContractInvocationRequest.html │ ├── client_api.CreateExternalTransferRequest.html │ ├── client_api.CreateFundOperationRequest.html │ ├── client_api.CreateFundQuoteRequest.html │ ├── client_api.CreatePayloadSignatureRequest.html │ ├── client_api.CreateServerSignerRequest.html │ ├── client_api.CreateSmartContractRequest.html │ ├── client_api.CreateSmartWalletRequest.html │ ├── client_api.CreateStakingOperationRequest.html │ ├── client_api.CreateTradeRequest.html │ ├── client_api.CreateTransferRequest.html │ ├── client_api.CreateUserOperationRequest.html │ ├── client_api.CreateWalletRequest.html │ ├── client_api.CreateWalletRequestWallet.html │ ├── client_api.CreateWalletWebhookRequest.html │ ├── client_api.CreateWebhookRequest.html │ ├── client_api.CryptoAmount.html │ ├── client_api.DeploySmartContractRequest.html │ ├── client_api.ERC20TransferEvent.html │ ├── client_api.ERC721TransferEvent.html │ ├── client_api.EthereumTokenTransfer.html │ ├── client_api.EthereumTransaction.html │ ├── client_api.EthereumTransactionAccess.html │ ├── client_api.EthereumTransactionAccessList.html │ ├── client_api.EthereumTransactionFlattenedTrace.html │ ├── client_api.EthereumValidatorMetadata.html │ ├── client_api.ExternalAddressesApiInterface.html │ ├── client_api.FaucetTransaction.html │ ├── client_api.FeatureSet.html │ ├── client_api.FetchHistoricalStakingBalances200Response.html │ ├── client_api.FetchStakingRewards200Response.html │ ├── client_api.FetchStakingRewardsRequest.html │ ├── client_api.FiatAmount.html │ ├── client_api.FundApiInterface.html │ ├── client_api.FundOperation.html │ ├── client_api.FundOperationFees.html │ ├── client_api.FundOperationList.html │ ├── client_api.FundQuote.html │ ├── client_api.GetStakingContextRequest.html │ ├── client_api.HistoricalBalance.html │ ├── client_api.MPCWalletStakeApiInterface.html │ ├── client_api.ModelError.html │ ├── client_api.MultiTokenContractOptions.html │ ├── client_api.NFTContractOptions.html │ ├── client_api.Network.html │ ├── client_api.NetworksApiInterface.html │ ├── client_api.OnchainIdentityApiInterface.html │ ├── client_api.OnchainName.html │ ├── client_api.OnchainNameList.html │ ├── client_api.PayloadSignature.html │ ├── client_api.PayloadSignatureList.html │ ├── client_api.ReadContractRequest.html │ ├── client_api.RegisterSmartContractRequest.html │ ├── client_api.ReputationApiInterface.html │ ├── client_api.SeedCreationEvent.html │ ├── client_api.SeedCreationEventResult.html │ ├── client_api.ServerSigner.html │ ├── client_api.ServerSignerEvent.html │ ├── client_api.ServerSignerEventList.html │ ├── client_api.ServerSignerList.html │ ├── client_api.ServerSignersApiInterface.html │ ├── client_api.SignatureCreationEvent.html │ ├── client_api.SignatureCreationEventResult.html │ ├── client_api.SignedVoluntaryExitMessageMetadata.html │ ├── client_api.SmartContract.html │ ├── client_api.SmartContractActivityEvent.html │ ├── client_api.SmartContractList.html │ ├── client_api.SmartContractsApiInterface.html │ ├── client_api.SmartWallet.html │ ├── client_api.SmartWalletList.html │ ├── client_api.SmartWalletsApiInterface.html │ ├── client_api.SolidityValue.html │ ├── client_api.SponsoredSend.html │ ├── client_api.StakeApiInterface.html │ ├── client_api.StakingBalance.html │ ├── client_api.StakingContext.html │ ├── client_api.StakingContextContext.html │ ├── client_api.StakingOperation.html │ ├── client_api.StakingReward.html │ ├── client_api.StakingRewardUSDValue.html │ ├── client_api.TokenContractOptions.html │ ├── client_api.Trade.html │ ├── client_api.TradeList.html │ ├── client_api.TradesApiInterface.html │ ├── client_api.Transaction.html │ ├── client_api.TransactionHistoryApiInterface.html │ ├── client_api.TransactionLog.html │ ├── client_api.TransactionReceipt.html │ ├── client_api.Transfer.html │ ├── client_api.TransferList.html │ ├── client_api.TransfersApiInterface.html │ ├── client_api.UpdateSmartContractRequest.html │ ├── client_api.UpdateWebhookRequest.html │ ├── client_api.User.html │ ├── client_api.UserOperation.html │ ├── client_api.UsersApiInterface.html │ ├── client_api.Validator.html │ ├── client_api.ValidatorList.html │ ├── client_api.Wallet.html │ ├── client_api.WalletList.html │ ├── client_api.WalletsApiInterface.html │ ├── client_api.Webhook.html │ ├── client_api.WebhookEventFilter.html │ ├── client_api.WebhookList.html │ ├── client_api.WebhookSmartContractEventFilter.html │ ├── client_api.WebhookWalletActivityFilter.html │ ├── client_api.WebhooksApiInterface.html │ ├── client_base.RequestArgs.html │ ├── client_configuration.ConfigurationParameters.html │ ├── coinbase_types.AddressReputationApiClient.html │ ├── coinbase_types.BalanceHistoryApiClient.html │ ├── coinbase_types.BroadcastExternalTransactionResponse.html │ ├── coinbase_types.FundOperationApiClient.html │ ├── coinbase_types.MnemonicSeedPhrase.html │ ├── coinbase_types.PaginationResponse.html │ ├── coinbase_types.SmartContractAPIClient.html │ ├── coinbase_types.TransactionHistoryApiClient.html │ ├── coinbase_types.WalletData.html │ └── coinbase_types.WebhookApiClient.html ├── modules │ ├── actions_sendUserOperation.html │ ├── actions_waitForUserOperation.html │ ├── client.html │ ├── client_api.html │ ├── client_base.html │ ├── client_common.html │ ├── client_configuration.html │ ├── coinbase_address.html │ ├── coinbase_address_external_address.html │ ├── coinbase_address_reputation.html │ ├── coinbase_address_wallet_address.html │ ├── coinbase_api_error.html │ ├── coinbase_asset.html │ ├── coinbase_authenticator.html │ ├── coinbase_balance.html │ ├── coinbase_balance_map.html │ ├── coinbase_coinbase.html │ ├── coinbase_constants.html │ ├── coinbase_contract_event.html │ ├── coinbase_contract_invocation.html │ ├── coinbase_crypto_amount.html │ ├── coinbase_errors.html │ ├── coinbase_faucet_transaction.html │ ├── coinbase_fiat_amount.html │ ├── coinbase_fund_operation.html │ ├── coinbase_fund_quote.html │ ├── coinbase_hash.html │ ├── coinbase_historical_balance.html │ ├── coinbase_payload_signature.html │ ├── coinbase_read_contract.html │ ├── coinbase_server_signer.html │ ├── coinbase_smart_contract.html │ ├── coinbase_sponsored_send.html │ ├── coinbase_staking_balance.html │ ├── coinbase_staking_operation.html │ ├── coinbase_staking_reward.html │ ├── coinbase_trade.html │ ├── coinbase_transaction.html │ ├── coinbase_transfer.html │ ├── coinbase_types.html │ ├── coinbase_types_contract.html │ ├── coinbase_utils.html │ ├── coinbase_validator.html │ ├── coinbase_wallet.html │ ├── coinbase_webhook.html │ ├── index.html │ ├── types_calls.html │ ├── types_chain.html │ ├── types_contract.html │ ├── types_misc.html │ ├── types_multicall.html │ ├── types_utils.html │ ├── utils_chain.html │ ├── utils_wait.html │ ├── wallets_createSmartWallet.html │ ├── wallets_toSmartWallet.html │ └── wallets_types.html ├── types │ ├── actions_sendUserOperation.SendUserOperationOptions.html │ ├── actions_sendUserOperation.SendUserOperationReturnType.html │ ├── actions_waitForUserOperation.CompletedOperation.html │ ├── actions_waitForUserOperation.FailedOperation.html │ ├── actions_waitForUserOperation.WaitForUserOperationOptions.html │ ├── actions_waitForUserOperation.WaitForUserOperationReturnType.html │ ├── client_api.FundOperationStatusEnum.html │ ├── client_api.NetworkProtocolFamilyEnum.html │ ├── client_api.PayloadSignatureStatusEnum.html │ ├── client_api.ResolveIdentityByAddressRolesEnum.html │ ├── client_api.ServerSignerEventEvent.html │ ├── client_api.SmartContractOptions.html │ ├── client_api.SolidityValueTypeEnum.html │ ├── client_api.SponsoredSendStatusEnum.html │ ├── client_api.StakingOperationMetadata.html │ ├── client_api.StakingOperationStatusEnum.html │ ├── client_api.StakingRewardStateEnum.html │ ├── client_api.TransactionContent.html │ ├── client_api.TransactionStatusEnum.html │ ├── client_api.UserOperationStatusEnum.html │ ├── client_api.ValidatorDetails.html │ ├── client_api.WalletServerSignerStatusEnum.html │ ├── client_api.WebhookEventTypeFilter.html │ ├── coinbase_types.AddressAPIClient.html │ ├── coinbase_types.Amount.html │ ├── coinbase_types.ApiClients.html │ ├── coinbase_types.AssetAPIClient.html │ ├── coinbase_types.CoinbaseConfigureFromJsonOptions.html │ ├── coinbase_types.CoinbaseOptions.html │ ├── coinbase_types.ContractInvocationAPIClient.html │ ├── coinbase_types.CreateContractInvocationOptions.html │ ├── coinbase_types.CreateCustomContractOptions.html │ ├── coinbase_types.CreateERC1155Options.html │ ├── coinbase_types.CreateERC20Options.html │ ├── coinbase_types.CreateERC721Options.html │ ├── coinbase_types.CreateFundOptions.html │ ├── coinbase_types.CreateQuoteOptions.html │ ├── coinbase_types.CreateTradeOptions.html │ ├── coinbase_types.CreateTransferOptions.html │ ├── coinbase_types.CreateWebhookOptions.html │ ├── coinbase_types.Destination.html │ ├── coinbase_types.ExternalAddressAPIClient.html │ ├── coinbase_types.ExternalSmartContractAPIClient.html │ ├── coinbase_types.ListHistoricalBalancesOptions.html │ ├── coinbase_types.ListHistoricalBalancesResult.html │ ├── coinbase_types.ListTransactionsOptions.html │ ├── coinbase_types.ListTransactionsResult.html │ ├── coinbase_types.MultiTokenContractOptions.html │ ├── coinbase_types.NFTContractOptions.html │ ├── coinbase_types.PaginationOptions.html │ ├── coinbase_types.RegisterContractOptions.html │ ├── coinbase_types.SeedData.html │ ├── coinbase_types.ServerSignerAPIClient.html │ ├── coinbase_types.SmartContractOptions.html │ ├── coinbase_types.SmartWalletAPIClient.html │ ├── coinbase_types.StakeAPIClient.html │ ├── coinbase_types.TokenContractOptions.html │ ├── coinbase_types.TradeApiClients.html │ ├── coinbase_types.TransferAPIClient.html │ ├── coinbase_types.TypedDataDomain.html │ ├── coinbase_types.TypedDataField.html │ ├── coinbase_types.UpdateContractOptions.html │ ├── coinbase_types.UpdateWebhookOptions.html │ ├── coinbase_types.WalletAPIClient.html │ ├── coinbase_types.WalletCreateOptions.html │ ├── coinbase_types.WalletStakeAPIClient.html │ ├── coinbase_types_contract.ContractFunctionReturnType.html │ ├── types_calls.Call.html │ ├── types_calls.Calls.html │ ├── types_chain.Network.html │ ├── types_chain.SupportedChainId.html │ ├── types_contract.ContractFunctionArgs.html │ ├── types_contract.ContractFunctionName.html │ ├── types_contract.ContractFunctionParameters.html │ ├── types_contract.ExtractAbiFunctionForArgs.html │ ├── types_contract.UnionWiden.html │ ├── types_contract.Widen.html │ ├── types_misc.Address.html │ ├── types_misc.Hash.html │ ├── types_misc.Hex.html │ ├── types_multicall.GetMulticallContractParameters.html │ ├── types_utils.Assign.html │ ├── types_utils.Evaluate.html │ ├── types_utils.ExactPartial.html │ ├── types_utils.ExactRequired.html │ ├── types_utils.Filter.html │ ├── types_utils.IsNarrowable.html │ ├── types_utils.IsNever.html │ ├── types_utils.IsUndefined.html │ ├── types_utils.IsUnion.html │ ├── types_utils.LooseOmit.html │ ├── types_utils.MaybePartial.html │ ├── types_utils.MaybePromise.html │ ├── types_utils.MaybeRequired.html │ ├── types_utils.Mutable.html │ ├── types_utils.NoInfer.html │ ├── types_utils.NoUndefined.html │ ├── types_utils.Omit.html │ ├── types_utils.OneOf.html │ ├── types_utils.Or.html │ ├── types_utils.PartialBy.html │ ├── types_utils.Prettify.html │ ├── types_utils.RequiredBy.html │ ├── types_utils.Some.html │ ├── types_utils.UnionEvaluate.html │ ├── types_utils.UnionLooseOmit.html │ ├── types_utils.UnionOmit.html │ ├── types_utils.UnionPartialBy.html │ ├── types_utils.UnionPick.html │ ├── types_utils.UnionRequiredBy.html │ ├── types_utils.UnionToTuple.html │ ├── types_utils.ValueOf.html │ ├── utils_wait.WaitOptions.html │ ├── wallets_createSmartWallet.CreateSmartWalletOptions.html │ ├── wallets_toSmartWallet.ToSmartWalletOptions.html │ ├── wallets_types.NetworkScopedSmartWallet.html │ ├── wallets_types.Signer.html │ ├── wallets_types.SmartWallet.html │ └── wallets_types.SmartWalletNetworkOptions.html └── variables │ ├── client_api.FundOperationStatusEnum-1.html │ ├── client_api.NetworkProtocolFamilyEnum-1.html │ ├── client_api.PayloadSignatureStatusEnum-1.html │ ├── client_api.ResolveIdentityByAddressRolesEnum-1.html │ ├── client_api.SolidityValueTypeEnum-1.html │ ├── client_api.SponsoredSendStatusEnum-1.html │ ├── client_api.StakingOperationStatusEnum-1.html │ ├── client_api.StakingRewardStateEnum-1.html │ ├── client_api.TransactionStatusEnum-1.html │ ├── client_api.UserOperationStatusEnum-1.html │ ├── client_api.WalletServerSignerStatusEnum-1.html │ ├── client_base.BASE_PATH.html │ ├── client_base.COLLECTION_FORMATS.html │ ├── client_base.operationServerMap.html │ ├── client_common.DUMMY_BASE_URL.html │ ├── coinbase_constants.GWEI_DECIMALS.html │ └── types_chain.CHAIN_ID_TO_NETWORK_ID.html ├── jest.config.js ├── package-lock.json ├── package.json ├── quickstart-template ├── README.md ├── bridge-usdc.js ├── discord_tutorial │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── app.cjs │ ├── package-lock.json │ ├── package.json │ ├── vercel.json │ └── webhook-transfer.js ├── index.js ├── mass-payout.js ├── package-lock.json ├── package.json ├── register-basename.js ├── solana_list_rewards.ts ├── solana_stake.ts ├── solana_wallet.ts ├── trade-assets.js ├── wallet-history.js ├── webhook-wallet-transfer.js ├── webhook.js └── webhook │ ├── README.md │ ├── app.js │ ├── package-lock.json │ └── package.json ├── src ├── actions │ ├── sendUserOperation.test.ts │ ├── sendUserOperation.ts │ ├── waitForUserOperation.test.ts │ └── waitForUserOperation.ts ├── client │ ├── api.ts │ ├── base.ts │ ├── common.ts │ ├── configuration.ts │ └── index.ts ├── coinbase │ ├── address.ts │ ├── address │ │ ├── external_address.ts │ │ └── wallet_address.ts │ ├── address_reputation.ts │ ├── api_error.ts │ ├── asset.ts │ ├── authenticator.ts │ ├── balance.ts │ ├── balance_map.ts │ ├── coinbase.ts │ ├── constants.ts │ ├── contract_event.ts │ ├── contract_invocation.ts │ ├── crypto_amount.ts │ ├── errors.ts │ ├── faucet_transaction.ts │ ├── fiat_amount.ts │ ├── fund_operation.ts │ ├── fund_quote.ts │ ├── hash.ts │ ├── historical_balance.ts │ ├── payload_signature.ts │ ├── read_contract.ts │ ├── server_signer.ts │ ├── smart_contract.ts │ ├── sponsored_send.ts │ ├── staking_balance.ts │ ├── staking_operation.ts │ ├── staking_reward.ts │ ├── trade.ts │ ├── transaction.ts │ ├── transfer.ts │ ├── types.ts │ ├── types │ │ └── contract.ts │ ├── utils.ts │ ├── validator.ts │ ├── wallet.ts │ └── webhook.ts ├── index.ts ├── tests │ ├── address_reputation_test.ts │ ├── address_test.ts │ ├── api_error_test.ts │ ├── asset_test.ts │ ├── authenticator_test.ts │ ├── balance_map_test.ts │ ├── balance_test.ts │ ├── coinbase_test.ts │ ├── config │ │ ├── invalid.json │ │ ├── not_parseable.json │ │ ├── test_api_key.json │ │ ├── test_api_key_with_only_id.json │ │ ├── test_ed25519_api_key.json │ │ └── test_ed25519_api_key_with_only_id.json │ ├── contract_event_test.ts │ ├── contract_invocation_test.ts │ ├── crypto_amount_test.ts │ ├── e2e.ts │ ├── error_test.ts │ ├── external_address_test.ts │ ├── faucet_transaction_test.ts │ ├── fiat_amount_test.ts │ ├── fund_operation_test.ts │ ├── fund_quote_test.ts │ ├── hash_test.ts │ ├── historical_balance_test.ts │ ├── index_test.ts │ ├── payload_signature_test.ts │ ├── read_contract_test.ts │ ├── server_signer_test.ts │ ├── smart_contract_test.ts │ ├── sponsored_send_test.ts │ ├── stake_test.ts │ ├── staking_historical_balance_test.ts │ ├── staking_operation_test.ts │ ├── staking_reward_test.ts │ ├── trade_test.ts │ ├── transaction_test.ts │ ├── transfer_test.ts │ ├── types.test-d.ts │ ├── utils.ts │ ├── utils_test.ts │ ├── validator_test.ts │ ├── wallet_address_fund_test.ts │ ├── wallet_address_test.ts │ ├── wallet_fund_test.ts │ ├── wallet_test.ts │ ├── wallet_transfer_test.ts │ └── webhook_test.ts ├── types │ ├── calls.ts │ ├── chain.ts │ ├── contract.ts │ ├── misc.ts │ ├── multicall.ts │ └── utils.ts ├── utils │ ├── chain.test.ts │ ├── chain.ts │ ├── wait.test.ts │ └── wait.ts └── wallets │ ├── createSmartWallet.test.ts │ ├── createSmartWallet.ts │ ├── toSmartWallet.test.ts │ ├── toSmartWallet.ts │ └── types.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:jsdoc/recommended" 8 | ], 9 | "plugins": ["@typescript-eslint", "prettier"], 10 | "env": { 11 | "node": true, 12 | "es6": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "multiline-comment-style": ["error", "starred-block"], 20 | "prettier/prettier": "error", 21 | "@typescript-eslint/member-ordering": "error", 22 | "jsdoc/tag-lines": ["error", "any", { "startLines": 1 }], 23 | "jsdoc/check-alignment": "error", 24 | "jsdoc/no-undefined-types": "off", 25 | "jsdoc/check-param-names": "error", 26 | "jsdoc/check-tag-names": "error", 27 | "jsdoc/check-types": "error", 28 | "jsdoc/implements-on-classes": "error", 29 | "jsdoc/require-description": "error", 30 | "jsdoc/require-jsdoc": [ 31 | "error", 32 | { 33 | "require": { 34 | "FunctionDeclaration": true, 35 | "MethodDefinition": true, 36 | "ClassDeclaration": true, 37 | "ArrowFunctionExpression": false, 38 | "FunctionExpression": false 39 | } 40 | } 41 | ], 42 | "jsdoc/require-param": "error", 43 | "jsdoc/require-param-description": "error", 44 | "jsdoc/require-param-type": "off", 45 | "jsdoc/require-returns": "error", 46 | "jsdoc/require-returns-description": "error", 47 | "jsdoc/require-returns-type": "off", 48 | "jsdoc/require-hyphen-before-param-description": ["error", "always"] 49 | }, 50 | "ignorePatterns": ["src/**/__tests__/**", "src/**/*.test.ts"] 51 | } 52 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What changed? Why? 2 | 3 | 4 | #### Qualified Impact 5 | 8 | -------------------------------------------------------------------------------- /.github/workflows/e2e_test.yml: -------------------------------------------------------------------------------- 1 | name: Run E2E Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | 17 | - name: Run E2E Tests 18 | env: 19 | NAME: ${{ secrets.NAME }} 20 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 21 | WALLET_DATA: ${{ secrets.WALLET_DATA }} 22 | STAKE_API_KEY_NAME: ${{ secrets.NAME }} 23 | STAKE_API_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 24 | STAKE_ADDRESS_ID_1: ${{ secrets.STAKE_ADDRESS_ID_1 }} 25 | STAKE_ADDRESS_ID_2: ${{ secrets.STAKE_ADDRESS_ID_2 }} 26 | STAKE_VALIDATOR_ADDRESS_1: ${{ secrets.STAKE_VALIDATOR_ADDRESS_1 }} 27 | run: npm run test:dry-run && npm run test:e2e 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Run Linters 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Run linters 21 | run: npm run lint 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to NPM 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | environment: npm 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | registry-url: "https://registry.npmjs.org" 17 | - run: npm install 18 | - run: npm ci 19 | - run: npm publish --provenance --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Run Unit Tests 21 | run: npm run test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | api.json 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | .DS_Store 135 | **/*_local* 136 | 137 | .idea 138 | 139 | quickstart-template/node_modules 140 | quickstart-template/wallet-array.csv 141 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | coverage/ 3 | .github/ 4 | src/client 5 | **/**/*.json 6 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": false, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "printWidth": 100, 10 | "proseWrap": "never" 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## Development 4 | 5 | ### Node.js Version 6 | 7 | Developing in this repository requires Node.js 18 or higher. 8 | 9 | ### Set-up 10 | 11 | Clone the repo by running: 12 | 13 | ```bash 14 | git clone git@github.com:coinbase/coinbase-sdk-nodejs.git 15 | ``` 16 | 17 | To install all dependencies, run: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | ### Linting 24 | 25 | To autocorrect all lint errors, run: 26 | 27 | ```bash 28 | npm run lint-fix 29 | ``` 30 | 31 | To detect all lint errors, run: 32 | 33 | ```bash 34 | npm run lint 35 | ``` 36 | 37 | ### Testing 38 | 39 | To run all tests, run: 40 | 41 | ```bash 42 | npm test 43 | ``` 44 | 45 | To run a specific test, run (for example): 46 | 47 | ```bash 48 | npx jest ./src/coinbase/tests/wallet_test.ts 49 | ``` 50 | To run e2e tests, run: 51 | 52 | In the root directory, create a .env file with the following configuration. Ensure to update the placeholders with your actual data: 53 | ```bash 54 | NAME=API_KEY_NAME 55 | PRIVATE_KEY=API_PRIVATE_KEY 56 | WALLET_DATA={ "WALLET_ID": { "seed": "", "encrypted": false, "authTag": "", "iv": "" } } 57 | ``` 58 | 59 | Then run the following commands: 60 | ```bash 61 | npm run test:dry-run && npm run test:e2e 62 | ``` 63 | 64 | ### Generating Documentation 65 | 66 | To generate documentation from the TypeDoc comments, run: 67 | 68 | ```bash 69 | npm run docs 70 | ``` 71 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache-2.0 License 2 | 3 | Copyright 2024 Coinbase 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | This project includes software from https://github.com/wevm/viem/ 2 | * Copyright (c) 2023-present weth, LLC 3 | * Licensed under MIT License -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The Coinbase team takes security seriously. Please do not file a public ticket discussing a potential vulnerability. 4 | 5 | Please report your findings through our [HackerOne][1] program. 6 | 7 | [1]: https://hackerone.com/coinbase -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #0000FF; 7 | --dark-hl-2: #569CD6; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #AF00DB; 15 | --dark-hl-6: #C586C0; 16 | --light-hl-7: #001080; 17 | --dark-hl-7: #9CDCFE; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #000000; 21 | --dark-hl-9: #C8C8C8; 22 | --light-hl-10: #267F99; 23 | --dark-hl-10: #4EC9B0; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | extensionsToTreatAsEsm: [".ts"], 5 | testMatch: ["**/src/**/*.test.ts", "**/src/tests/**/*.ts"], 6 | coveragePathIgnorePatterns: ["node_modules", "client", "__tests__", "/src/tests/"], 7 | collectCoverage: true, 8 | collectCoverageFrom: ["./src/**/*.ts"], 9 | coverageReporters: ["html"], 10 | verbose: true, 11 | maxWorkers: 1, 12 | coverageThreshold: { 13 | "./src/**/*.ts": { 14 | branches: 75, 15 | functions: 85, 16 | statements: 85, 17 | lines: 85, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coinbase/coinbase-sdk", 3 | "author": "Coinbase Inc.", 4 | "license": "ISC", 5 | "description": "Coinbase Platform SDK", 6 | "repository": "https://github.com/coinbase/coinbase-sdk-nodejs", 7 | "version": "0.25.0", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "lint": "eslint -c .eslintrc.json src/coinbase/**.ts", 12 | "lint-fix": "eslint -c .eslintrc.json src/coinbase/*.ts --fix", 13 | "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", 14 | "format-check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", 15 | "check": "tsc --noEmit", 16 | "test": "jest --config jest.config.js --testPathIgnorePatterns src/tests/e2e.ts src/tests/utils.ts src/tests/types.test-d.ts", 17 | "test:dry-run": "npm install && npm ci && npm publish --dry-run", 18 | "test:e2e": "npx jest --no-cache --testMatch=**/e2e.ts --coverageThreshold '{}'", 19 | "test:e2e:stake": "npx jest --no-cache --testMatch=**/e2e.ts --coverageThreshold '{}' -t Stake", 20 | "test:types": "tsd --files src/tests/types.test-d.ts", 21 | "clean": "rm -rf dist/*", 22 | "build": "tsc", 23 | "prepack": "tsc --skipLibCheck", 24 | "docs": "typedoc --entryPoints ./src --entryPointStrategy expand --exclude ./src/tests/**/*.ts" 25 | }, 26 | "files": [ 27 | "dist" 28 | ], 29 | "dependencies": { 30 | "@scure/bip32": "^1.4.0", 31 | "abitype": "^1.0.6", 32 | "axios": "^1.6.8", 33 | "axios-mock-adapter": "^1.22.0", 34 | "axios-retry": "^4.4.1", 35 | "bip32": "^4.0.0", 36 | "bip39": "^3.1.0", 37 | "decimal.js": "^10.4.3", 38 | "dotenv": "^16.4.5", 39 | "ed2curve": "^0.3.0", 40 | "ethers": "^6.12.1", 41 | "jose": "^5.10.0", 42 | "secp256k1": "^5.0.0", 43 | "viem": "^2.21.26" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "^29.5.12", 47 | "@types/node": "^20.12.11", 48 | "@types/secp256k1": "^4.0.6", 49 | "@typescript-eslint/eslint-plugin": "^7.8.0", 50 | "@typescript-eslint/parser": "^7.8.0", 51 | "eslint": "^8.57.0", 52 | "eslint-config-prettier": "^9.1.0", 53 | "eslint-plugin-jsdoc": "^48.2.5", 54 | "eslint-plugin-prettier": "^5.1.3", 55 | "jest": "^29.7.0", 56 | "mock-fs": "^5.2.0", 57 | "prettier": "^3.2.5", 58 | "ts-jest": "^29.1.2", 59 | "ts-node": "^10.9.2", 60 | "tsd": "^0.31.2", 61 | "typedoc": "^0.25.13", 62 | "typescript": "^5.4.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /quickstart-template/README.md: -------------------------------------------------------------------------------- 1 | # Quickstart template for Platform SDK 2 | 3 | This is a template repository for quickly getting started with the Platform SDK. It provides a simple example of how to use the SDK. 4 | 5 | ## Create a Wallet, Fund, and Transfer 6 | 7 | To set up the template, run the following commands: 8 | 9 | ```bash 10 | npm install 11 | npm run start 12 | ``` 13 | 14 | This command will create a developer-custodial wallet, deposit testnet funds to it and perform a transfer to another wallet. 15 | 16 | ## Wallet transaction history 17 | 18 | To set up the template, run the following commands: 19 | 20 | ```bash 21 | npm install 22 | npm run start-wallet-history 23 | ``` 24 | 25 | This command will create a developer-custodial wallet, deposit testnet funds to it and perform a few transfers to another wallet and back. Then list all the transactions on that wallet. 26 | 27 | ## Trade Assets 28 | 29 | To set up the template, run the following commands: 30 | 31 | ```bash 32 | npm install 33 | npm run start-trade-assets 34 | ``` 35 | 36 | This command will create a developer-custodial wallet on Base Mainnet and trade ETH for USDC. 37 | 38 | ## Mass Payout 39 | 40 | To set up the template, run the following commands: 41 | 42 | ```bash 43 | npm install 44 | npm run start-mass-payout 45 | ``` 46 | 47 | This command will demonstrate how to automatically send batched payments from a CSV file with a non-MPC API Wallet. 48 | 49 | ## Webhook 50 | 51 | If you don't already have a URL setup for event notification, 52 | you can follow these [instructions to setup a simple Webhook App](./webhook/README.md). 53 | 54 | To set up the template, run the following commands: 55 | 56 | ```bash 57 | npm install 58 | npm run start-webhook 59 | ``` 60 | 61 | This command will demonstrate how to create a webhook for ERC20 transfer events on USDC. 62 | 63 | You can also use [CDP Portal](https://portal.cdp.coinbase.com/products/webhooks) for Webhook configurations. 64 | 65 | ### Webhook - transfer between wallets 66 | 67 | We also have a template for setting up two wallets and a webhook and receiving the transfer information between those two wallets on your webhook. 68 | 69 | To set up the template, run the following commands: 70 | 71 | ```bash 72 | npm install 73 | npm run start-webhook-wallet-transfer 74 | ``` 75 | 76 | On this template, we'll demonstrate how to do a ERC20 transfer between two wallets and receive that transfer on your webhook. 77 | 78 | _Note: Although usually transactions are sent to webhook within a minute, it may take several minutes for it to be sent to the webhook._ 79 | 80 | You can find more information about webhooks in the [documentation](https://docs.cdp.coinbase.com/onchain-data/docs/webhooks). 81 | 82 | _____________________ 83 | -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_URL="https://DISCORD_URL" 2 | WEBHOOK_NOTIFICATION_URL="https://YOUR_NOTIFICATION_URL" -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/README.md: -------------------------------------------------------------------------------- 1 | # CDP Webhooks Discord bot 2 | 3 | This repo contains a bot that posts messages to Discord whenever you receive a message on your webhook. 4 | 5 | More info on the docs: https://docs.cdp.coinbase.com/get-started/docs/webhooks/discord-bot-demo 6 | 7 | ## Prerequisites 8 | 9 | You'll need: 10 | 11 | - [CDP API Key](https://portal.cdp.coinbase.com/access/api) 12 | - A [Discord webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) URL 13 | 14 | ## How to run 15 | 16 | 1. First install packages: `npm install` 17 | 18 | 2. Setup tunneling, using Pinggy: `npm run start-pinggy` 19 | 20 | 3. Copy .env.example into .env file on the same folder and replace the `DISCORD_URL` with your Discord webhook URL and `WEBHOOK_NOTIFICATION_URL` with your pinggy HTTPS URL from previous step. 21 | 22 | 4. Run your webhook server: `npm run start-server` 23 | 24 | 5. The last step is to execute the transfer using our SDK: `npm run start-transfer` 25 | 26 | If everything was successful, after a few seconds, you should see the transfer event data being posted to your Discord channel. 27 | -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/app.cjs: -------------------------------------------------------------------------------- 1 | require("dotenv/config"); 2 | const express = require("express"); 3 | const axios = require("axios"); 4 | const bodyParser = require("body-parser"); 5 | 6 | const app = express(); 7 | const jsonParser = bodyParser.json(); 8 | 9 | app.get("/", jsonParser, (req, res) => { 10 | res.send("Your https server is working!"); 11 | }); 12 | 13 | app.post("/", jsonParser, (req, res) => { 14 | if (!process.env.DISCORD_URL) { 15 | console.log("DISCORD_URL is missing from env"); 16 | res.sendStatus(400); 17 | return; 18 | } 19 | 20 | const data = req.body; 21 | 22 | let messageContent = "A new " + data.eventType + " event was received from the webhook: \n```"; 23 | messageContent += JSON.stringify(data, null, 2); 24 | messageContent += "```\n"; 25 | messageContent += `Data received at ${new Date().toLocaleString("en-US")}`; 26 | 27 | const postData = { 28 | content: messageContent, 29 | }; 30 | axios 31 | .post(process.env.DISCORD_URL, postData) 32 | .then(() => { 33 | console.log("Successfully posted message to discord"); 34 | res.sendStatus(200); 35 | }) 36 | .catch(e => { 37 | console.error(e); 38 | res.sendStatus(400); 39 | }); 40 | }); 41 | 42 | app.listen(5000, () => { 43 | console.log("Running on port 5000."); 44 | }); 45 | 46 | module.exports = app; 47 | -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord_quick2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start-server": "node app.cjs", 8 | "start-transfer": "node webhook-transfer.js", 9 | "start-pinggy": "ssh -p 443 -R0:localhost:5000 a.pinggy.io" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "type": "module", 16 | "dependencies": { 17 | "@coinbase/coinbase-sdk": "^0.11.0", 18 | "axios": "^1.7.7", 19 | "body-parser": "^1.20.3", 20 | "dotenv": "^16.4.5", 21 | "express": "^4.21.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "app.cjs", 6 | "use": "@now/node" 7 | } 8 | ], 9 | "routes": [ 10 | 11 | { 12 | "src": "/(.*)", 13 | "dest": "app.cjs" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /quickstart-template/discord_tutorial/webhook-transfer.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { Coinbase, Webhook, Wallet } from "@coinbase/coinbase-sdk"; 3 | import fs from "fs"; 4 | 5 | // Change this to the path of your API key file downloaded from CDP portal. 6 | Coinbase.configureFromJson({ filePath: "/Users/jairdarosajunior/Downloads/cdp_api_key.json" }); 7 | const webhookNotificationUri = process.env.WEBHOOK_NOTIFICATION_URL; 8 | 9 | (async function () { 10 | if (!webhookNotificationUri) { 11 | console.log("WEBHOOK_NOTIFICATION_URL is missing from env file."); 12 | return; 13 | } 14 | const seedPath = "wallet_saved_seeds.json"; 15 | 16 | let myWallet; 17 | let anotherWallet = await Wallet.create(); 18 | 19 | // If Wallet exists, load 20 | if (fs.existsSync(seedPath)) { 21 | console.log("🔄 Wallet exists, re-instantiating..."); 22 | const seedData = transformConfig(seedPath); 23 | myWallet = await Wallet.import(seedData); 24 | console.log("✅ Wallet re-instantiated!"); 25 | } 26 | // Create Wallet 27 | else { 28 | myWallet = await Wallet.create(); 29 | const saveSeed = myWallet.saveSeedToFile(seedPath); 30 | console.log("✅ Seed saved: ", saveSeed); 31 | } 32 | 33 | const balance = await myWallet.getBalance(Coinbase.assets.Usdc); 34 | console.log(`💰 Wallet USDC balance:`, balance); 35 | if (balance <= 0) { 36 | // If wallet doesn't have funds we need to add funds to it 37 | const faucetTx = await myWallet.faucet(Coinbase.assets.Usdc); 38 | 39 | // Wait for the faucet transaction to confirm. 40 | await faucetTx.wait(); 41 | 42 | console.log("✅ Funds added!"); 43 | 44 | // Sometimes funds take a few seconds to be available on the wallet, so lets wait 5 secs 45 | await sleep(5000); 46 | } 47 | 48 | // Now use below code to get wallets addresses so we can use it for adding it to the webhook filter. 49 | let myWalletAddress = await myWallet.getDefaultAddress(); 50 | const myWalletAddressId = myWalletAddress.getId(); 51 | 52 | console.log("💳 myWallet address: ", myWalletAddressId); 53 | 54 | const webhooks = await Webhook.list(); 55 | let shouldCreateWebhook = !webhookAlreadyExists(webhooks); 56 | 57 | if (shouldCreateWebhook) { 58 | console.log("🔄 Creating webhook..."); 59 | await Webhook.create({ 60 | networkId: Coinbase.networks.BaseSepolia, 61 | notificationUri: webhookNotificationUri, 62 | eventType: "wallet_activity", 63 | eventTypeFilter: { 64 | addresses: [myWalletAddressId], 65 | }, 66 | }); 67 | console.log("✅ Webhook created!"); 68 | } else { 69 | console.log("⏩ Skipping Webhook creation..."); 70 | } 71 | 72 | // For testing this above example, let's now create a transfer between both wallets: 73 | // Create transfer from myWallet to anotherWallet 74 | const transfer = await myWallet.createTransfer({ 75 | amount: 0.0001, 76 | assetId: Coinbase.assets.Usdc, 77 | destination: anotherWallet, 78 | gasless: true, // for USDC, you can add gasless flag, so you don't need to add ETH funds for paying for gas fees 79 | }); 80 | 81 | // Wait for the transfer to complete or fail on-chain 82 | await transfer.wait({ 83 | intervalSeconds: 1, // check for transfer completion each 1 second 84 | timeoutSeconds: 30, // keep checking for 30 seconds 85 | }); 86 | console.log("✅ Transfer was successful: ", transfer.toString()); 87 | })(); 88 | 89 | // ========================== UTILS FUNCTIONS =================================== 90 | function webhookAlreadyExists(webhooks) { 91 | for (let currentWebhook of webhooks.data) { 92 | if ( 93 | currentWebhook.getEventType() === "wallet_activity" && 94 | currentWebhook.getNotificationURI() === webhookNotificationUri 95 | ) { 96 | return true; 97 | } 98 | } 99 | return false; 100 | } 101 | 102 | function transformConfig(filePath) { 103 | try { 104 | const rawData = fs.readFileSync(filePath, "utf-8"); 105 | const originalConfig = JSON.parse(rawData); 106 | const walletId = Object.keys(originalConfig)[0]; 107 | const { seed } = originalConfig[walletId]; 108 | return { 109 | walletId, 110 | seed, 111 | }; 112 | } catch (error) { 113 | console.error("Error reading or parsing file:", error); 114 | throw error; 115 | } 116 | } 117 | 118 | function sleep(ms) { 119 | return new Promise(resolve => setTimeout(resolve, ms)); 120 | } 121 | -------------------------------------------------------------------------------- /quickstart-template/index.js: -------------------------------------------------------------------------------- 1 | import { Coinbase, Wallet } from "@coinbase/coinbase-sdk"; 2 | 3 | // Change this to the path of your API key file downloaded from CDP portal. 4 | Coinbase.configureFromJson({ filePath: "~/Downloads/cdp_api_key.json" }); 5 | 6 | // Create a Wallet for the User. 7 | let wallet = await Wallet.create(); 8 | console.log(`Wallet successfully created: `, wallet.toString()); 9 | 10 | // Wallets come with a single default Address, accessible via getDefaultAddress: 11 | let address = await wallet.getDefaultAddress(); 12 | console.log(`Default address for the wallet: `, address.toString()); 13 | 14 | const faucetTransaction = await wallet.faucet(); 15 | 16 | // Wait for the faucet transaction to complete or fail on-chain. 17 | await faucetTransaction.wait(); 18 | console.log(`Faucet transaction successfully completed: `, faucetTransaction.toString()); 19 | 20 | let anotherWallet = await Wallet.create(); 21 | console.log(`Second Wallet successfully created: `, anotherWallet.toString()); 22 | 23 | const transfer = await wallet.createTransfer({ 24 | amount: 0.00001, 25 | assetId: Coinbase.assets.Eth, 26 | destination: anotherWallet, 27 | }); 28 | 29 | // Wait for the transfer to complete or fail on-chain. 30 | await transfer.wait(); 31 | 32 | console.log(`Transfer successfully completed: `, transfer.toString()); 33 | -------------------------------------------------------------------------------- /quickstart-template/mass-payout.js: -------------------------------------------------------------------------------- 1 | import { Coinbase, Wallet } from "@coinbase/coinbase-sdk"; 2 | import { createArrayCsvWriter } from "csv-writer"; 3 | import os from "os"; 4 | import fs from "fs"; 5 | import { parse } from "csv-parse"; 6 | 7 | // Create receiving Wallets. 8 | async function createReceivingWallets() { 9 | // Create 5 receiving Wallets and only store Wallet Addresses. 10 | const addresses = []; 11 | 12 | for (let i = 1; i <= 5; i++) { 13 | let receivingWallet = await Wallet.create(); 14 | console.log(`Receiving Wallet${i} successfully created: `, receivingWallet.toString()); 15 | 16 | let receivingAddress = await receivingWallet.getDefaultAddress(); 17 | console.log(`Default address for Wallet${i}: `, receivingAddress.getId()); 18 | addresses.push([receivingAddress.getId()]); // Storing Address as an array. 19 | } 20 | 21 | return addresses; 22 | } 23 | 24 | // Write to CSV file with receiving Wallet Addresses. 25 | async function writeReceivingAddressesToCsv(addresses) { 26 | // Define CSV file. 27 | const csvWriter = createArrayCsvWriter({ 28 | path: "wallet-array.csv", 29 | header: false, 30 | }); 31 | 32 | // Write Wallet Addresses to CSV file. 33 | await csvWriter.writeRecords(addresses); 34 | console.log("The CSV file was written successfully without headers."); 35 | } 36 | 37 | // Create and fund a sending Wallet. 38 | async function createAndFundSendingWallet() { 39 | // Create sending Wallet. 40 | let sendingWallet = await Wallet.create(); 41 | console.log(`sendingWallet successfully created: `, sendingWallet.toString()); 42 | 43 | // Get sending Wallet Address. 44 | let sendingAddress = await sendingWallet.getDefaultAddress(); 45 | console.log(`Default address for sendingWallet: `, sendingAddress.toString()); 46 | 47 | // Fund sending Wallet. 48 | const faucetTransaction = await sendingWallet.faucet(); 49 | 50 | // Wait for the faucet transaction to complete or fail on-chain. 51 | await faucetTransaction.wait(); 52 | console.log(`Faucet transaction successfully completed: `, faucetTransaction.toString()); 53 | 54 | return sendingWallet; 55 | } 56 | 57 | // Read from CSV file and send mass payout. 58 | async function sendMassPayout(sendingWallet) { 59 | // Define amount to send. 60 | const transferAmount = 0.000002; 61 | const assetId = Coinbase.assets.Eth; 62 | 63 | try { 64 | const parser = fs 65 | .createReadStream("./wallet-array.csv") 66 | .pipe(parse({ delimiter: ",", from_line: 1 })); 67 | 68 | for await (const row of parser) { 69 | const address = row[0]; 70 | if (address) { 71 | try { 72 | const transfer = await sendingWallet.createTransfer({ 73 | // Send payment to each Address in CSV. 74 | amount: transferAmount, 75 | assetId: assetId, 76 | destination: address, 77 | }); 78 | 79 | await transfer.wait(); 80 | 81 | console.log(`Transfer to ${address} successful`); 82 | } catch (error) { 83 | console.error(`Error transferring to ${address}: `, error); 84 | } 85 | } 86 | } 87 | } catch (error) { 88 | console.error(`Error processing CSV file: `, error); 89 | } 90 | 91 | console.log("Finished processing CSV file"); 92 | } 93 | 94 | (async () => { 95 | try { 96 | // Manage CDP Api Key for Coinbase SDK. 97 | // Configure location to CDP API Key. 98 | Coinbase.configureFromJson({ 99 | filePath: `${os.homedir()}/Downloads/cdp_api_key.json`, 100 | }); 101 | 102 | const addresses = await createReceivingWallets(); 103 | await writeReceivingAddressesToCsv(addresses); 104 | const sendingWallet = await createAndFundSendingWallet(); 105 | await sendMassPayout(sendingWallet); 106 | } catch (error) { 107 | console.error(`Error in sending mass payout: `, error); 108 | } 109 | })(); 110 | -------------------------------------------------------------------------------- /quickstart-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickstart-template", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start-webhook-wallet-transfer": "node webhook-wallet-transfer.js", 8 | "start-wallet-history": "node wallet-history.js", 9 | "start": "node index.js", 10 | "start-trade-assets": "node trade-assets.js", 11 | "start-mass-payout": "node mass-payout.js", 12 | "start-webhook": "node webhook.js", 13 | "solana-list-rewards": "tsx solana_list_rewards.ts", 14 | "solana-stake": "tsx solana_stake.ts", 15 | "start-bridge-usdc": "node bridge-usdc.js", 16 | "start-register-basename": "node register-basename.js" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "type": "module", 22 | "dependencies": { 23 | "@solana/web3.js": "^2.0.0-rc.1", 24 | "bs58": "^6.0.0", 25 | "@coinbase/coinbase-sdk": "^0.25.0", 26 | "csv-parse": "^5.5.6", 27 | "csv-writer": "^1.6.0", 28 | "viem": "^2.21.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /quickstart-template/solana_list_rewards.ts: -------------------------------------------------------------------------------- 1 | import { StakingReward, Coinbase, Address } from "@coinbase/coinbase-sdk"; 2 | import { coinbaseApiKeyPath, getKeypair } from "./solana_wallet"; 3 | 4 | // list solana staking rewards for the provided wallet on solana-mainnet 5 | // then get the historical staking balances for the same wallet. 6 | // use May 2024 until current the today's date. 7 | async function listSolanaStakingRewards(wallet: string) { 8 | const startTime = new Date(2024, 5).toISOString(); 9 | 10 | const rewards = await StakingReward.list( 11 | "solana-mainnet", 12 | Coinbase.assets.Sol, 13 | [wallet], 14 | startTime, 15 | new Date().toISOString(), 16 | ); 17 | console.log(rewards); 18 | 19 | const addr = new Address("solana-mainnet", wallet); 20 | const balances = await addr.historicalStakingBalances( 21 | Coinbase.assets.Sol, 22 | startTime, 23 | new Date().toISOString(), 24 | ); 25 | console.log(balances); 26 | } 27 | 28 | // Initialize the sdk with the api key 29 | Coinbase.configureFromJson({ filePath: coinbaseApiKeyPath() }); 30 | 31 | // GetKeypair will try to read the default path of the solana CLI keypair 32 | const keyPair = getKeypair(); 33 | keyPair.then(kp => { 34 | listSolanaStakingRewards(kp.address); 35 | }); 36 | -------------------------------------------------------------------------------- /quickstart-template/solana_stake.ts: -------------------------------------------------------------------------------- 1 | import { Coinbase } from "@coinbase/coinbase-sdk"; 2 | import { coinbaseApiKeyPath, getKeypair } from "./solana_wallet"; 3 | import { 4 | KeyPairSigner, 5 | createSolanaRpc, 6 | getBase64EncodedWireTransaction, 7 | getTransactionDecoder, 8 | signTransaction, 9 | } from "@solana/web3.js"; 10 | import * as bs58 from "bs58"; 11 | import { NetworkIdentifier } from "@coinbase/coinbase-sdk/dist/client"; 12 | 13 | // Get data about staking for the provided wallet, get stakable balance, unstakeable balances and 14 | // balances that can be claimed back. 15 | async function stakeOperations(signer: KeyPairSigner) { 16 | const ctx = ( 17 | await Coinbase.apiClients.stake?.getStakingContext({ 18 | network_id: "solana-devnet", 19 | address_id: signer.address.toString(), 20 | asset_id: Coinbase.assets.Sol, 21 | options: {}, 22 | }) 23 | )?.data.context; 24 | 25 | console.log("Stakeable: "); 26 | console.log(ctx?.stakeable_balance.amount); 27 | console.log(ctx?.stakeable_balance.asset); 28 | let ops: Array = new Array(); 29 | 30 | { 31 | const amount = ctx?.stakeable_balance.amount 32 | ? BigInt(ctx?.stakeable_balance.amount.toString()) 33 | : 0; 34 | if (amount > 1) 35 | ops.push( 36 | (await Coinbase.apiClients.stake!.buildStakingOperation({ 37 | action: "stake", 38 | address_id: signer.address.toString(), 39 | asset_id: Coinbase.assets.Sol, 40 | network_id: NetworkIdentifier.SolanaDevnet, 41 | options: { amount: "100000000" }, 42 | }))!.data!, 43 | ); 44 | } 45 | 46 | console.log("Unstakeable: "); 47 | console.log(ctx?.unstakeable_balance.amount); 48 | console.log(ctx?.unstakeable_balance.asset); 49 | 50 | { 51 | const amount = ctx?.unstakeable_balance.amount 52 | ? BigInt(ctx?.unstakeable_balance.amount.toString()) 53 | : 0; 54 | if (amount > 1) 55 | ops.push( 56 | (await Coinbase.apiClients.stake!.buildStakingOperation({ 57 | action: "unstake", 58 | address_id: signer.address.toString(), 59 | asset_id: Coinbase.assets.Sol, 60 | network_id: NetworkIdentifier.SolanaDevnet, 61 | options: { amount: "100000000" }, 62 | }))!.data!, 63 | ); 64 | } 65 | 66 | console.log("Claimable: "); 67 | console.log(ctx?.claimable_balance.amount); 68 | console.log(ctx?.claimable_balance.asset); 69 | 70 | { 71 | const amount = ctx?.claimable_balance.amount 72 | ? BigInt(ctx?.claimable_balance.amount.toString()) 73 | : 0; 74 | if (amount > 1) 75 | ops.push( 76 | (await Coinbase.apiClients.stake!.buildStakingOperation({ 77 | action: "claim_stake", 78 | address_id: signer.address.toString(), 79 | asset_id: Coinbase.assets.Sol, 80 | network_id: NetworkIdentifier.SolanaDevnet, 81 | options: { amount: "100000000" }, 82 | }))!.data!, 83 | ); 84 | } 85 | 86 | const rpc = createSolanaRpc("https://api.devnet.solana.com"); 87 | 88 | for (let op of ops) { 89 | for (let tx of op.transactions) { 90 | let txmsg = getTransactionDecoder().decode(bs58.default.decode(tx.unsigned_payload)); 91 | let signed = await signTransaction([signer.keyPair], txmsg); 92 | let sig = await rpc 93 | .sendTransaction(getBase64EncodedWireTransaction(signed), { encoding: "base64" }) 94 | .send(); 95 | 96 | console.log(`txLink: ${getTxLink(NetworkIdentifier.SolanaDevnet, sig)}`); 97 | } 98 | } 99 | } 100 | 101 | function getTxLink(networkID: string, signature: string): string { 102 | if (networkID == "solana-mainnet") { 103 | return "https://explorer.solana.com/tx/" + signature; 104 | } else if (networkID == "solana-devnet") { 105 | return "https://explorer.solana.com/tx/" + signature + "?cluster=devnet"; 106 | } 107 | 108 | return ""; 109 | } 110 | 111 | // Initialize the sdk with the api key 112 | Coinbase.configureFromJson({ filePath: coinbaseApiKeyPath() }); 113 | 114 | // GetKeypair will try to read the default path of the solana CLI keypair 115 | const keyPair = getKeypair(); 116 | keyPair.then(kp => { 117 | stakeOperations(kp); 118 | }); 119 | -------------------------------------------------------------------------------- /quickstart-template/solana_wallet.ts: -------------------------------------------------------------------------------- 1 | import { KeyPairSigner, createKeyPairSignerFromBytes } from "@solana/web3.js"; 2 | import { readFileSync } from "fs"; 3 | import { homedir } from "os"; 4 | import { env } from "process"; 5 | 6 | const DEFAULT_SOLANA_CLI_WALLET_PATH = "~/.config/solana/id.json"; 7 | const CB_KEYPAIR_ENV_VAR = "COINBASE_SDK_KEY"; 8 | const DEFAULT_KEY_PATH = "~/.config/coinbase/api-key.json"; 9 | 10 | // Looks for the api key in the home config dir if it's not present it will look 11 | // for the path on the COINBASE_SDK_KEY env var. 12 | export function coinbaseApiKeyPath(): string { 13 | const key = env[CB_KEYPAIR_ENV_VAR]; 14 | 15 | return key ? key : DEFAULT_KEY_PATH; 16 | } 17 | 18 | // Get a solana-cli compatible key from disk, defaults to the default solana-cli wallet path. 19 | export async function getKeypair(path?: string): Promise { 20 | const bs = readFileSync(replaceHome(path ? path : DEFAULT_SOLANA_CLI_WALLET_PATH)); 21 | 22 | const pkBytes = new Uint8Array(JSON.parse(bs.toString())); 23 | 24 | return await createKeyPairSignerFromBytes(pkBytes); 25 | } 26 | 27 | function replaceHome(filePath: string): string { 28 | return filePath.startsWith("~") ? filePath.replace("~", homedir()) : filePath; 29 | } 30 | -------------------------------------------------------------------------------- /quickstart-template/trade-assets.js: -------------------------------------------------------------------------------- 1 | import { Coinbase, Wallet } from "@coinbase/coinbase-sdk"; 2 | 3 | Coinbase.configureFromJson({ filePath: "~/Downloads/cdp_api_key.json" }); 4 | 5 | // Create a Wallet on base-mainnet to trade assets with. 6 | let wallet = await Wallet.create({ networkId: Coinbase.networks.BaseMainnet }); 7 | 8 | /* 9 | * Fund the Wallet's default Address with ETH from an external source. 10 | * Trade 0.00001 ETH to USDC. 11 | */ 12 | let trade = await wallet.createTrade(0.00001, Coinbase.assets.Eth, Coinbase.assets.Usdc); 13 | 14 | // Wait for the trade to complete or fail on-chain. 15 | await trade.wait(); 16 | 17 | console.log(`Trade successfully completed: `, trade.toString()); 18 | -------------------------------------------------------------------------------- /quickstart-template/webhook.js: -------------------------------------------------------------------------------- 1 | import { Coinbase, Webhook } from "@coinbase/coinbase-sdk"; 2 | 3 | // Change this to the path of your API key file downloaded from CDP portal. 4 | Coinbase.configureFromJson({ filePath: "~/Downloads/cdp_api_key.json" }); 5 | 6 | // Be sure to update the uri to your webhook url 7 | let webhook = await Webhook.create( 8 | "base-mainnet", 9 | "https:///callback", 10 | "erc20_transfer", 11 | [{ contract_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" }], 12 | ); 13 | console.log(`Webhook successfully created: `, webhook.toString()); 14 | 15 | // List out the webhooks created 16 | let resp = await Webhook.list(); 17 | 18 | // Iterate over all webhooks created 19 | // You can also see list of webhook from CDP Portal 20 | // https://portal.cdp.coinbase.com/products/webhooks 21 | for (const wh of resp.data) { 22 | console.log(wh.toString()); 23 | } 24 | -------------------------------------------------------------------------------- /quickstart-template/webhook/README.md: -------------------------------------------------------------------------------- 1 | # Quickstart template for Webhook App 2 | 3 | This is a template repository for quickly getting started with a Webhook App, 4 | which can be used for setting up Webhooks for listening to blockchain event notifications. 5 | 6 | ## Local Webhook App 7 | 8 | To set up the template, run the following commands from this `webhook` folder: 9 | 10 | ```bash 11 | npm install 12 | npm run start-webhook-app 13 | ``` 14 | 15 | This command will set up a webhook app locally. 16 | 17 | Once the local webhook app is setup, in another terminal window, you can test it with: 18 | 19 | ```bash 20 | curl -X POST -H "Content-Type:application/json" -d '{"app": "webhook"}' http://localhost:3000/callback 21 | ``` 22 | 23 | ## Public Webhook App 24 | 25 | To setup a temporary public URL that points to this local webhook app, 26 | you can use [Pinggy](https://pinggy.io/) or [ngrok](https://ngrok.com/) in another terminal window: 27 | 28 | ```bash 29 | ssh -p 443 -R0:localhost:3000 -L4300:localhost:4300 qr@a.pinggy.io 30 | ``` 31 | 32 | You can also use [Vercel](https://vercel.com/), [webhook.site](https://webhook.site/) or other hosting solutions for your webhook app. 33 | 34 | Once the public webhook app is setup, copy the URL provided and test it with: 35 | 36 | ```bash 37 | curl -X POST -H "Content-Type:application/json" -d '{"app": "webhook"}' {url_copied_from_pinggy_io}/callback 38 | ``` 39 | 40 | This URL (ie Notification URL) can now be used to create Webhook either 41 | via [CDP Portal](https://docs-cdp-onchain-data-preview.cbhq.net/developer-platform/docs/cdp-webhooks/) 42 | or [Coinbase SDK](https://docs-cdp-onchain-data-preview.cbhq.net/coinbase-sdk/docs/webhooks). 43 | -------------------------------------------------------------------------------- /quickstart-template/webhook/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | 4 | app.use(express.json()); 5 | 6 | app.post("/callback", (req, res) => { 7 | // here's what you'll expect to receive depending on the event type 8 | // https://docs.cdp.coinbase.com/onchain-data/docs/webhooks#event-types 9 | const data = req.body; 10 | 11 | console.log("Headers received:"); 12 | console.log(JSON.stringify(req.headers, null, 4)); 13 | console.log("Body received:"); 14 | console.log(JSON.stringify(data, null, 4)); 15 | 16 | const response = { 17 | message: "Data received", 18 | received_data: data, 19 | }; 20 | res.json(response); 21 | }); 22 | 23 | const PORT = 3000; 24 | app.listen(PORT, () => { 25 | console.log(`Server is running on port ${PORT}`); 26 | }); 27 | -------------------------------------------------------------------------------- /quickstart-template/webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start-webhook-app": "node app.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.19.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/sendUserOperation.ts: -------------------------------------------------------------------------------- 1 | import type { SmartWallet } from "../wallets/types"; 2 | import { UserOperationStatusEnum } from "../client"; 3 | import { CHAIN_ID_TO_NETWORK_ID, type SupportedChainId } from "../types/chain"; 4 | import type { Address, Hex } from "../types/misc"; 5 | import type { Calls } from "../types/calls"; 6 | import { encodeFunctionData } from "viem"; 7 | import { Coinbase } from "../coinbase/coinbase"; 8 | 9 | /** 10 | * Options for sending a user operation 11 | * @template T - Array type for the calls parameter 12 | */ 13 | export type SendUserOperationOptions = { 14 | /** 15 | * Array of contract calls to execute in the user operation. 16 | * Each call can either be: 17 | * - A direct call with `to`, `value`, and `data` 18 | * - A contract call with `to`, `abi`, `functionName`, and `args` 19 | * 20 | * @example 21 | * ```ts 22 | * const calls = [ 23 | * { 24 | * to: "0x1234567890123456789012345678901234567890", 25 | * value: parseEther("0.0000005"), 26 | * data: "0x", 27 | * }, 28 | * { 29 | * to: "0x1234567890123456789012345678901234567890", 30 | * abi: erc20Abi, 31 | * functionName: "transfer", 32 | * args: [to, amount], 33 | * }, 34 | * ] 35 | * ``` 36 | */ 37 | calls: Calls; 38 | /** Chain ID of the network to execute on */ 39 | chainId: SupportedChainId; 40 | /** Optional URL of the paymaster service to use for gas sponsorship. Must be ERC-7677 compliant. */ 41 | paymasterUrl?: string; 42 | }; 43 | 44 | /** 45 | * Return type for the sendUserOperation function 46 | */ 47 | export type SendUserOperationReturnType = { 48 | /** The address of the smart wallet */ 49 | smartWalletAddress: Address; 50 | /** The status of the user operation */ 51 | status: typeof UserOperationStatusEnum.Broadcast; 52 | /** The hash of the user operation. This is not the transaction hash which is only available after the operation is completed.*/ 53 | userOpHash: Hex; 54 | }; 55 | 56 | /** 57 | * Sends a user operation to the network 58 | * 59 | * @example 60 | * ```ts 61 | * import { sendUserOperation } from "@coinbase/coinbase-sdk"; 62 | * import { parseEther } from "viem"; 63 | * 64 | * const result = await sendUserOperation(wallet, { 65 | * calls: [ 66 | * { 67 | * abi: erc20Abi, 68 | * functionName: "transfer", 69 | * args: [to, amount], 70 | * }, 71 | * { 72 | * to: "0x1234567890123456789012345678901234567890", 73 | * data: "0x", 74 | * value: parseEther("0.0000005"), 75 | * }, 76 | * ], 77 | * chainId: 1, 78 | * paymasterUrl: "https://api.developer.coinbase.com/rpc/v1/base/someapikey", 79 | * }); 80 | * ``` 81 | * 82 | * @param {SmartWallet} wallet - The smart wallet to send the user operation from 83 | * @param {SendUserOperationOptions} options - The options for the user operation 84 | * @returns {Promise} The result of the user operation 85 | */ 86 | export async function sendUserOperation( 87 | wallet: SmartWallet, 88 | options: SendUserOperationOptions, 89 | ): Promise { 90 | const { calls, chainId, paymasterUrl } = options; 91 | const network = CHAIN_ID_TO_NETWORK_ID[chainId]; 92 | 93 | if (calls.length === 0) { 94 | throw new Error("Calls array is empty"); 95 | } 96 | 97 | const encodedCalls = calls.map(call => { 98 | const value = (call.value ?? BigInt(0)).toString(); 99 | 100 | if ("abi" in call && call.abi && "functionName" in call) { 101 | return { 102 | to: call.to, 103 | data: encodeFunctionData({ 104 | abi: call.abi, 105 | functionName: call.functionName, 106 | args: call.args, 107 | }), 108 | value, 109 | }; 110 | } 111 | 112 | return { 113 | to: call.to, 114 | data: call.data ?? "0x", 115 | value, 116 | }; 117 | }); 118 | 119 | const createOpResponse = await Coinbase.apiClients.smartWallet!.createUserOperation( 120 | wallet.address, 121 | network, 122 | { 123 | calls: encodedCalls, 124 | paymaster_url: paymasterUrl, 125 | }, 126 | ); 127 | 128 | const owner = wallet.owners[0]; 129 | 130 | const signature = await owner.sign({ 131 | hash: createOpResponse.data.user_op_hash as Hex, 132 | }); 133 | 134 | const broadcastResponse = await Coinbase.apiClients.smartWallet!.broadcastUserOperation( 135 | wallet.address, 136 | createOpResponse.data.user_op_hash, 137 | { 138 | signature, 139 | }, 140 | ); 141 | 142 | return { 143 | smartWalletAddress: wallet.address, 144 | status: broadcastResponse.data.status, 145 | userOpHash: createOpResponse.data.user_op_hash, 146 | } as SendUserOperationReturnType; 147 | } 148 | -------------------------------------------------------------------------------- /src/actions/waitForUserOperation.test.ts: -------------------------------------------------------------------------------- 1 | import { waitForUserOperation } from "./waitForUserOperation"; 2 | import { Coinbase } from "../coinbase/coinbase"; 3 | import { UserOperationStatusEnum } from "../client"; 4 | import { smartWalletApiMock, mockReturnValue } from "../tests/utils"; 5 | import * as waitUtils from "../utils/wait"; 6 | 7 | describe("waitForUserOperation", () => { 8 | const VALID_WALLET_ADDRESS = "0x1234567890123456789012345678901234567890" as const; 9 | 10 | const VALID_OPERATION_HASH = "0x1234567890123456789012345678901234567890" as const; 11 | const VALID_OPERATION_RESPONSE = { 12 | status: UserOperationStatusEnum.Complete, 13 | transaction_hash: "0x1234567890123456789012345678901234567890", 14 | user_op_hash: VALID_OPERATION_HASH, 15 | }; 16 | 17 | const FAILED_OPERATION_RESPONSE = { 18 | smartWalletAddress: VALID_WALLET_ADDRESS, 19 | status: UserOperationStatusEnum.Failed, 20 | user_op_hash: VALID_OPERATION_HASH, 21 | }; 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | Coinbase.apiClients.smartWallet = smartWalletApiMock; 26 | Coinbase.apiClients.smartWallet!.getUserOperation = mockReturnValue(VALID_OPERATION_RESPONSE); 27 | }); 28 | 29 | afterEach(() => { 30 | jest.restoreAllMocks(); 31 | }); 32 | 33 | it("should successfully wait for a completed operation", async () => { 34 | const result = await waitForUserOperation({ 35 | userOpHash: VALID_OPERATION_HASH, 36 | smartWalletAddress: VALID_WALLET_ADDRESS, 37 | }); 38 | 39 | expect(Coinbase.apiClients.smartWallet!.getUserOperation).toHaveBeenCalledWith( 40 | VALID_WALLET_ADDRESS, 41 | VALID_OPERATION_HASH, 42 | ); 43 | 44 | expect(result).toEqual({ 45 | smartWalletAddress: VALID_WALLET_ADDRESS, 46 | status: UserOperationStatusEnum.Complete, 47 | transactionHash: "0x1234567890123456789012345678901234567890", 48 | userOpHash: VALID_OPERATION_HASH, 49 | }); 50 | }); 51 | 52 | it("should successfully handle a failed operation", async () => { 53 | Coinbase.apiClients.smartWallet!.getUserOperation = mockReturnValue(FAILED_OPERATION_RESPONSE); 54 | 55 | const result = await waitForUserOperation({ 56 | userOpHash: VALID_OPERATION_HASH, 57 | smartWalletAddress: VALID_WALLET_ADDRESS, 58 | }); 59 | 60 | expect(Coinbase.apiClients.smartWallet!.getUserOperation).toHaveBeenCalledWith( 61 | VALID_WALLET_ADDRESS, 62 | VALID_OPERATION_HASH, 63 | ); 64 | 65 | expect(result).toEqual({ 66 | smartWalletAddress: VALID_WALLET_ADDRESS, 67 | status: UserOperationStatusEnum.Failed, 68 | transactionHash: undefined, 69 | userOpHash: VALID_OPERATION_HASH, 70 | }); 71 | }); 72 | 73 | it("should use default timeout options when none are provided", async () => { 74 | const waitSpy = jest.spyOn(waitUtils, "wait"); 75 | 76 | const result = await waitForUserOperation({ 77 | userOpHash: VALID_OPERATION_HASH, 78 | smartWalletAddress: VALID_WALLET_ADDRESS, 79 | }); 80 | 81 | expect(waitSpy).toHaveBeenCalledWith( 82 | expect.any(Function), 83 | expect.any(Function), 84 | expect.any(Function), 85 | { timeoutSeconds: 30 }, 86 | ); 87 | 88 | expect(result).toEqual({ 89 | smartWalletAddress: VALID_WALLET_ADDRESS, 90 | status: UserOperationStatusEnum.Complete, 91 | transactionHash: "0x1234567890123456789012345678901234567890", 92 | userOpHash: VALID_OPERATION_HASH, 93 | }); 94 | }); 95 | 96 | it("should respect custom timeout options", async () => { 97 | const waitSpy = jest.spyOn(waitUtils, "wait"); 98 | 99 | const result = await waitForUserOperation({ 100 | userOpHash: VALID_OPERATION_HASH, 101 | smartWalletAddress: VALID_WALLET_ADDRESS, 102 | waitOptions: { timeoutSeconds: 1, intervalSeconds: 0.1 }, 103 | }); 104 | 105 | expect(waitSpy).toHaveBeenCalledWith( 106 | expect.any(Function), 107 | expect.any(Function), 108 | expect.any(Function), 109 | { timeoutSeconds: 1, intervalSeconds: 0.1 }, 110 | ); 111 | 112 | expect(result).toEqual({ 113 | smartWalletAddress: VALID_WALLET_ADDRESS, 114 | status: UserOperationStatusEnum.Complete, 115 | transactionHash: "0x1234567890123456789012345678901234567890", 116 | userOpHash: VALID_OPERATION_HASH, 117 | }); 118 | }); 119 | 120 | it("should throw an error if the operation is not terminal", async () => { 121 | Coinbase.apiClients.smartWallet!.getUserOperation = mockReturnValue({ 122 | user_op_hash: VALID_OPERATION_HASH, 123 | status: UserOperationStatusEnum.Pending, 124 | }); 125 | 126 | await expect( 127 | waitForUserOperation({ 128 | userOpHash: VALID_OPERATION_HASH, 129 | smartWalletAddress: VALID_WALLET_ADDRESS, 130 | waitOptions: { timeoutSeconds: 1 }, 131 | }), 132 | ).rejects.toThrow( 133 | "Operation has not reached a terminal state after 1 seconds and may still succeed. Retry with a longer timeout using the timeoutSeconds option.", 134 | ); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/actions/waitForUserOperation.ts: -------------------------------------------------------------------------------- 1 | import type { Address, Hex } from "../types/misc"; 2 | import { Coinbase } from "../coinbase/coinbase"; 3 | import { wait, WaitOptions } from "../utils/wait"; 4 | import { UserOperation, UserOperationStatusEnum } from "../client"; 5 | 6 | /** 7 | * Options for waiting for a user operation 8 | */ 9 | export type WaitForUserOperationOptions = { 10 | /** The hash of the user operation */ 11 | userOpHash: Hex; 12 | /** The address of the smart wallet */ 13 | smartWalletAddress: Address; 14 | /** Optional options for the wait operation */ 15 | waitOptions?: WaitOptions; 16 | }; 17 | 18 | /** 19 | * Represents a failed user operation 20 | */ 21 | export type FailedOperation = { 22 | /** The address of the smart wallet */ 23 | smartWalletAddress: Address; 24 | /** The status of the user operation */ 25 | status: typeof UserOperationStatusEnum.Failed; 26 | /** The hash of the user operation. This is not the transaction hash which is only available after the operation is completed.*/ 27 | userOpHash: Hex; 28 | }; 29 | 30 | /** 31 | * Represents a completed user operation 32 | */ 33 | export type CompletedOperation = { 34 | /** The address of the smart wallet */ 35 | smartWalletAddress: Address; 36 | /** The transaction hash that executed the completed user operation */ 37 | transactionHash: string; 38 | /** The status of the user operation */ 39 | status: typeof UserOperationStatusEnum.Complete; 40 | /** The hash of the user operation. This is not the transaction hash which is only available after the operation is completed.*/ 41 | userOpHash: Hex; 42 | }; 43 | 44 | /** 45 | * Represents the return type of the waitForUserOperation function 46 | */ 47 | export type WaitForUserOperationReturnType = FailedOperation | CompletedOperation; 48 | 49 | /** 50 | * Waits for a user operation to complete or fail 51 | * 52 | * @example 53 | * ```ts 54 | * import { waitForUserOperation } from "@coinbase/coinbase-sdk"; 55 | * 56 | * const result = await waitForUserOperation({ 57 | * id: "123", 58 | * smartWalletAddress: "0x1234567890123456789012345678901234567890", 59 | * waitOptions: { 60 | * timeoutSeconds: 30, 61 | * }, 62 | * }); 63 | * ``` 64 | * 65 | * @param {WaitForUserOperationOptions} options - The options for the wait operation 66 | * @returns {Promise} The result of the user operation 67 | */ 68 | export async function waitForUserOperation( 69 | options: WaitForUserOperationOptions, 70 | ): Promise { 71 | const { userOpHash, smartWalletAddress } = options; 72 | 73 | const reload = async () => { 74 | const response = await Coinbase.apiClients.smartWallet!.getUserOperation( 75 | smartWalletAddress, 76 | userOpHash, 77 | ); 78 | return response.data; 79 | }; 80 | 81 | const transform = (operation: UserOperation): WaitForUserOperationReturnType => { 82 | if (operation.status === UserOperationStatusEnum.Failed) { 83 | return { 84 | smartWalletAddress: smartWalletAddress, 85 | status: UserOperationStatusEnum.Failed, 86 | userOpHash: operation.user_op_hash as Hex, 87 | } satisfies FailedOperation; 88 | } else if (operation.status === UserOperationStatusEnum.Complete) { 89 | return { 90 | smartWalletAddress: smartWalletAddress, 91 | transactionHash: operation.transaction_hash!, 92 | status: UserOperationStatusEnum.Complete, 93 | userOpHash: operation.user_op_hash as Hex, 94 | } satisfies CompletedOperation; 95 | } else { 96 | throw new Error("User operation is not terminal"); 97 | } 98 | }; 99 | 100 | const waitOptions = options.waitOptions || { 101 | timeoutSeconds: 30, 102 | }; 103 | 104 | return await wait(reload, isTerminal, transform, waitOptions); 105 | } 106 | 107 | const isTerminal = (operation: UserOperation): boolean => { 108 | return ( 109 | operation.status === UserOperationStatusEnum.Complete || 110 | operation.status === UserOperationStatusEnum.Failed 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/client/base.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Coinbase Platform API 5 | * This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs. 6 | * 7 | * The version of the OpenAPI document: 0.0.1-alpha 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | import type { Configuration } from './configuration'; 17 | // Some imports not used depending on template conditions 18 | // @ts-ignore 19 | import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; 20 | import globalAxios from 'axios'; 21 | 22 | export const BASE_PATH = "https://api.cdp.coinbase.com/platform".replace(/\/+$/, ""); 23 | 24 | /** 25 | * 26 | * @export 27 | */ 28 | export const COLLECTION_FORMATS = { 29 | csv: ",", 30 | ssv: " ", 31 | tsv: "\t", 32 | pipes: "|", 33 | }; 34 | 35 | /** 36 | * 37 | * @export 38 | * @interface RequestArgs 39 | */ 40 | export interface RequestArgs { 41 | url: string; 42 | options: RawAxiosRequestConfig; 43 | } 44 | 45 | /** 46 | * 47 | * @export 48 | * @class BaseAPI 49 | */ 50 | export class BaseAPI { 51 | protected configuration: Configuration | undefined; 52 | 53 | constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { 54 | if (configuration) { 55 | this.configuration = configuration; 56 | this.basePath = configuration.basePath ?? basePath; 57 | } 58 | } 59 | }; 60 | 61 | /** 62 | * 63 | * @export 64 | * @class RequiredError 65 | * @extends {Error} 66 | */ 67 | export class RequiredError extends Error { 68 | constructor(public field: string, msg?: string) { 69 | super(msg); 70 | this.name = "RequiredError" 71 | } 72 | } 73 | 74 | interface ServerMap { 75 | [key: string]: { 76 | url: string, 77 | description: string, 78 | }[]; 79 | } 80 | 81 | /** 82 | * 83 | * @export 84 | */ 85 | export const operationServerMap: ServerMap = { 86 | } 87 | -------------------------------------------------------------------------------- /src/client/configuration.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Coinbase Platform API 5 | * This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs. 6 | * 7 | * The version of the OpenAPI document: 0.0.1-alpha 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | export interface ConfigurationParameters { 17 | apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); 18 | username?: string; 19 | password?: string; 20 | accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); 21 | basePath?: string; 22 | serverIndex?: number; 23 | baseOptions?: any; 24 | formDataCtor?: new () => any; 25 | } 26 | 27 | export class Configuration { 28 | /** 29 | * parameter for apiKey security 30 | * @param name security name 31 | * @memberof Configuration 32 | */ 33 | apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); 34 | /** 35 | * parameter for basic security 36 | * 37 | * @type {string} 38 | * @memberof Configuration 39 | */ 40 | username?: string; 41 | /** 42 | * parameter for basic security 43 | * 44 | * @type {string} 45 | * @memberof Configuration 46 | */ 47 | password?: string; 48 | /** 49 | * parameter for oauth2 security 50 | * @param name security name 51 | * @param scopes oauth2 scope 52 | * @memberof Configuration 53 | */ 54 | accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); 55 | /** 56 | * override base path 57 | * 58 | * @type {string} 59 | * @memberof Configuration 60 | */ 61 | basePath?: string; 62 | /** 63 | * override server index 64 | * 65 | * @type {number} 66 | * @memberof Configuration 67 | */ 68 | serverIndex?: number; 69 | /** 70 | * base options for axios calls 71 | * 72 | * @type {any} 73 | * @memberof Configuration 74 | */ 75 | baseOptions?: any; 76 | /** 77 | * The FormData constructor that will be used to create multipart form data 78 | * requests. You can inject this here so that execution environments that 79 | * do not support the FormData class can still run the generated client. 80 | * 81 | * @type {new () => FormData} 82 | */ 83 | formDataCtor?: new () => any; 84 | 85 | constructor(param: ConfigurationParameters = {}) { 86 | this.apiKey = param.apiKey; 87 | this.username = param.username; 88 | this.password = param.password; 89 | this.accessToken = param.accessToken; 90 | this.basePath = param.basePath; 91 | this.serverIndex = param.serverIndex; 92 | this.baseOptions = { 93 | headers: { 94 | ...param.baseOptions?.headers, 95 | 'User-Agent': "OpenAPI-Generator/typescript-axios" 96 | }, 97 | ...param.baseOptions 98 | }; 99 | this.formDataCtor = param.formDataCtor; 100 | } 101 | 102 | /** 103 | * Check if the given MIME is a JSON MIME. 104 | * JSON MIME examples: 105 | * application/json 106 | * application/json; charset=UTF8 107 | * APPLICATION/JSON 108 | * application/vnd.company+json 109 | * @param mime - MIME (Multipurpose Internet Mail Extensions) 110 | * @return True if the given MIME is JSON, false otherwise. 111 | */ 112 | public isJsonMime(mime: string): boolean { 113 | const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); 114 | return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Coinbase Platform API 5 | * This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs. 6 | * 7 | * The version of the OpenAPI document: 0.0.1-alpha 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | export * from "./api"; 17 | export * from "./configuration"; 18 | 19 | -------------------------------------------------------------------------------- /src/coinbase/address_reputation.ts: -------------------------------------------------------------------------------- 1 | import { AddressReputation as AddressReputationModel, AddressReputationMetadata } from "../client"; 2 | 3 | /** 4 | * A representation of the reputation of a blockchain address. 5 | */ 6 | export class AddressReputation { 7 | private model: AddressReputationModel; 8 | /** 9 | * A representation of the reputation of a blockchain address. 10 | * 11 | * @param {AddressReputationModel} model - The reputation model instance. 12 | */ 13 | constructor(model: AddressReputationModel) { 14 | if (!model) { 15 | throw new Error("Address reputation model cannot be empty"); 16 | } 17 | this.model = model; 18 | } 19 | 20 | /** 21 | * Returns the address ID. 22 | * 23 | * @returns {string} The address ID. 24 | */ 25 | public get risky(): boolean { 26 | return this.model.score < 0; 27 | } 28 | 29 | /** 30 | * Returns the score of the address. 31 | * The score is a number between -100 and 100. 32 | * 33 | * @returns {number} The score of the address. 34 | */ 35 | public get score(): number { 36 | return this.model.score; 37 | } 38 | 39 | /** 40 | * Returns the metadata of the address reputation. 41 | * The metadata contains additional information about the address reputation. 42 | * 43 | * @returns {AddressReputationMetadata} The metadata of the address reputation. 44 | */ 45 | public get metadata(): AddressReputationMetadata { 46 | return this.model.metadata; 47 | } 48 | 49 | /** 50 | * Returns the address ID. 51 | * 52 | * @returns {string} The address ID. 53 | */ 54 | toString(): string { 55 | const metadata = Object.entries(this.model.metadata).map(([key, value]) => { 56 | return `${key}: ${value}`; 57 | }); 58 | return `AddressReputation(score: ${this.score}, metadata: {${metadata.join(", ")}})`; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/coinbase/asset.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { Asset as AssetModel } from "./../client/api"; 3 | import { Coinbase } from "./coinbase"; 4 | import { GWEI_DECIMALS } from "./constants"; 5 | import { ArgumentError } from "./errors"; 6 | 7 | /** A representation of an Asset. */ 8 | export class Asset { 9 | public readonly networkId: string; 10 | public readonly assetId: string; 11 | public readonly contractAddress: string; 12 | public readonly decimals: number; 13 | 14 | /** 15 | * Private constructor for the Asset class. 16 | * 17 | * @param networkId - The network ID. 18 | * @param assetId - The asset ID. 19 | * @param contractAddress - The address ID. 20 | * @param decimals - The number of decimals. 21 | */ 22 | private constructor( 23 | networkId: string, 24 | assetId: string, 25 | contractAddress: string, 26 | decimals: number, 27 | ) { 28 | this.networkId = networkId; 29 | this.assetId = assetId; 30 | this.contractAddress = contractAddress; 31 | this.decimals = decimals; 32 | } 33 | 34 | /** 35 | * Creates an Asset from an Asset Model. 36 | * 37 | * @param model - The Asset Model. 38 | * @param assetId - The Asset ID. 39 | * @throws If the Asset Model is invalid. 40 | * @returns The Asset Class. 41 | */ 42 | public static fromModel(model: AssetModel, assetId?: string) { 43 | if (!model) { 44 | throw new Error("Invalid asset model"); 45 | } 46 | 47 | let decimals = model.decimals!; 48 | // TODO: Push this logic down to the backend. 49 | if (assetId && model.asset_id) { 50 | const normalizedModelAssetId = model.asset_id.toLowerCase(); 51 | const normalizedAssetId = assetId.toLowerCase(); 52 | 53 | if (Coinbase.toAssetId(normalizedModelAssetId) !== Coinbase.toAssetId(normalizedAssetId)) { 54 | switch (normalizedAssetId) { 55 | case "gwei": 56 | decimals = GWEI_DECIMALS; 57 | break; 58 | case "wei": 59 | decimals = 0; 60 | break; 61 | case "eth": 62 | break; 63 | default: 64 | throw new ArgumentError(`Invalid asset ID: ${assetId}`); 65 | } 66 | } 67 | } 68 | return new Asset( 69 | model.network_id, 70 | assetId ?? model.asset_id, 71 | model.contract_address!, 72 | decimals, 73 | ); 74 | } 75 | 76 | /** 77 | * Fetches the Asset with the provided Asset ID. 78 | * 79 | * @param networkId - The network ID. 80 | * @param assetId - The asset ID. 81 | * @throws If the Asset cannot be fetched. 82 | * @returns The Asset Class. 83 | */ 84 | static async fetch(networkId: string, assetId: string) { 85 | const asset = await Coinbase.apiClients.asset!.getAsset( 86 | Coinbase.normalizeNetwork(networkId), 87 | Asset.primaryDenomination(assetId), 88 | ); 89 | return Asset.fromModel(asset?.data, assetId); 90 | } 91 | 92 | /** 93 | * Returns the primary denomination for the provided Asset ID. 94 | * For `gwei` and `wei` the primary denomination is `eth`. 95 | * For all other assets, the primary denomination is the same asset ID. 96 | * 97 | * @param assetId - The Asset ID. 98 | * @returns The primary denomination for the Asset ID. 99 | */ 100 | public static primaryDenomination(assetId: string): string { 101 | return [Coinbase.assets.Gwei, Coinbase.assets.Wei].includes(assetId) 102 | ? Coinbase.assets.Eth 103 | : assetId; 104 | } 105 | 106 | /** 107 | * Returns the primary denomination for the Asset. 108 | * 109 | * @returns The primary denomination for the Asset. 110 | */ 111 | public primaryDenomination(): string { 112 | return Asset.primaryDenomination(this.assetId); 113 | } 114 | 115 | /** 116 | * Converts the amount of the Asset from whole to atomic units. 117 | * 118 | * @param wholeAmount - The whole amount to convert to atomic units. 119 | * @returns The amount in atomic units 120 | */ 121 | toAtomicAmount(wholeAmount: Decimal): bigint { 122 | const atomicAmount = wholeAmount.times(new Decimal(10).pow(this.decimals)); 123 | return BigInt(atomicAmount.toFixed()); 124 | } 125 | 126 | /** 127 | * Converts the amount of the Asset from atomic to whole units. 128 | * 129 | * @param atomicAmount - The atomic amount to convert to whole units. 130 | * @returns The amount in atomic units 131 | */ 132 | fromAtomicAmount(atomicAmount: Decimal): Decimal { 133 | return atomicAmount.dividedBy(new Decimal(10).pow(this.decimals)); 134 | } 135 | 136 | /** 137 | * Returns a string representation of the Asset. 138 | * 139 | * @returns a string representation of the Asset 140 | */ 141 | toString(): string { 142 | return `Asset{ networkId: ${this.networkId}, assetId: ${this.assetId}, contractAddress: ${this.contractAddress}, decimals: ${this.decimals} }`; 143 | } 144 | 145 | /** 146 | * Returns the Asset ID. 147 | * 148 | * @returns The Asset ID. 149 | */ 150 | getAssetId(): string { 151 | return this.assetId; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/coinbase/balance.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { Balance as BalanceModel } from "../client"; 3 | import { Asset } from "./asset"; 4 | 5 | /** A representation of a balance. */ 6 | export class Balance { 7 | public readonly amount: Decimal; 8 | public readonly assetId: string; 9 | public readonly asset?: Asset; 10 | 11 | /** 12 | * Private constructor to prevent direct instantiation outside of the factory methods. 13 | * 14 | * @ignore 15 | * @param {Decimal} amount - The amount of the balance. 16 | * @param {string} assetId - The asset ID. 17 | * @hideconstructor 18 | */ 19 | private constructor(amount: Decimal, assetId: string, asset?: Asset) { 20 | this.amount = amount; 21 | this.assetId = assetId; 22 | this.asset = asset; 23 | } 24 | 25 | /** 26 | * Converts a BalanceModel into a Balance object. 27 | * 28 | * @param {BalanceModel} model - The balance model object. 29 | * @returns {Balance} The Balance object. 30 | */ 31 | public static fromModel(model: BalanceModel): Balance { 32 | const asset = Asset.fromModel(model.asset); 33 | return new Balance( 34 | asset.fromAtomicAmount(new Decimal(model.amount)), 35 | asset.getAssetId(), 36 | asset, 37 | ); 38 | } 39 | 40 | /** 41 | * Converts a BalanceModel and asset ID into a Balance object. 42 | * 43 | * @param {BalanceModel} model - The balance model object. 44 | * @param {string} assetId - The asset ID. 45 | * @returns {Balance} The Balance object. 46 | */ 47 | public static fromModelAndAssetId(model: BalanceModel, assetId: string): Balance { 48 | const asset = Asset.fromModel(model.asset, assetId); 49 | return new Balance( 50 | asset.fromAtomicAmount(new Decimal(model.amount)), 51 | asset.getAssetId(), 52 | asset, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/coinbase/balance_map.ts: -------------------------------------------------------------------------------- 1 | import { Balance } from "./balance"; 2 | import { Balance as BalanceModel } from "../client"; 3 | import { Decimal } from "decimal.js"; 4 | 5 | /** 6 | * A convenience class for storing and manipulating Asset balances in a human-readable format. 7 | */ 8 | export class BalanceMap extends Map { 9 | /** 10 | * Converts a list of Balance models to a BalanceMap. 11 | * 12 | * @param {BalanceModel[]} balances - The list of balances fetched from the API. 13 | * @returns {BalanceMap} The converted BalanceMap object. 14 | */ 15 | public static fromBalances(balances: BalanceModel[]): BalanceMap { 16 | const balanceMap = new BalanceMap(); 17 | balances.forEach(balanceModel => { 18 | const balance = Balance.fromModel(balanceModel); 19 | balanceMap.add(balance); 20 | }); 21 | return balanceMap; 22 | } 23 | 24 | /** 25 | * Adds a balance to the map. 26 | * 27 | * @param {Balance} balance - The balance to add to the map. 28 | */ 29 | public add(balance: Balance): void { 30 | if (!(balance instanceof Balance)) { 31 | throw new Error("balance must be a Balance"); 32 | } 33 | this.set(balance.assetId, balance.amount); 34 | } 35 | 36 | /** 37 | * Returns a string representation of the balance map. 38 | * 39 | * @returns The string representation of the balance map. 40 | */ 41 | public toString(): string { 42 | const result: Record = {}; 43 | this.forEach((value, key) => { 44 | let str = value.toString(); 45 | if (value.isInteger()) { 46 | str = value.toNumber().toString(); 47 | } 48 | result[key] = str; 49 | }); 50 | return `BalanceMap${JSON.stringify(result)}`; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/coinbase/constants.ts: -------------------------------------------------------------------------------- 1 | export const GWEI_DECIMALS = 9; 2 | -------------------------------------------------------------------------------- /src/coinbase/contract_event.ts: -------------------------------------------------------------------------------- 1 | import { ContractEvent as ContractEventModel } from "../client"; 2 | /** 3 | * A representation of a single contract event. 4 | */ 5 | export class ContractEvent { 6 | private model: ContractEventModel; 7 | 8 | /** 9 | * Creates the ContractEvent object. 10 | * 11 | * @param model - The underlying contract event object. 12 | */ 13 | constructor(model: ContractEventModel) { 14 | this.model = model; 15 | } 16 | 17 | /** 18 | * Returns the network ID of the ContractEvent. 19 | * 20 | * @returns The network ID. 21 | */ 22 | public networkId(): string { 23 | return this.model.network_id; 24 | } 25 | 26 | /** 27 | * Returns the protocol name of the ContractEvent. 28 | * 29 | * @returns The protocol name. 30 | */ 31 | public protocolName(): string { 32 | return this.model.protocol_name; 33 | } 34 | 35 | /** 36 | * Returns the contract name of the ContractEvent. 37 | * 38 | * @returns The contract name. 39 | */ 40 | public contractName(): string { 41 | return this.model.contract_name; 42 | } 43 | 44 | /** 45 | * Returns the event name of the ContractEvent. 46 | * 47 | * @returns The event name. 48 | */ 49 | public eventName(): string { 50 | return this.model.event_name; 51 | } 52 | 53 | /** 54 | * Returns the signature of the ContractEvent. 55 | * 56 | * @returns The event signature. 57 | */ 58 | public sig(): string { 59 | return this.model.sig; 60 | } 61 | 62 | /** 63 | * Returns the four bytes of the Keccak hash of the event signature. 64 | * 65 | * @returns The four bytes of the event signature hash. 66 | */ 67 | public fourBytes(): string { 68 | return this.model.four_bytes; 69 | } 70 | 71 | /** 72 | * Returns the contract address of the ContractEvent. 73 | * 74 | * @returns The contract address. 75 | */ 76 | public contractAddress(): string { 77 | return this.model.contract_address; 78 | } 79 | 80 | /** 81 | * Returns the block time of the ContractEvent. 82 | * 83 | * @returns The block time. 84 | */ 85 | public blockTime(): Date { 86 | return new Date(this.model.block_time); 87 | } 88 | 89 | /** 90 | * Returns the block height of the ContractEvent. 91 | * 92 | * @returns The block height. 93 | */ 94 | public blockHeight(): number { 95 | return this.model.block_height; 96 | } 97 | 98 | /** 99 | * Returns the transaction hash of the ContractEvent. 100 | * 101 | * @returns The transaction hash. 102 | */ 103 | public txHash(): string { 104 | return this.model.tx_hash; 105 | } 106 | 107 | /** 108 | * Returns the transaction index of the ContractEvent. 109 | * 110 | * @returns The transaction index. 111 | */ 112 | public txIndex(): number { 113 | return this.model.tx_index; 114 | } 115 | 116 | /** 117 | * Returns the event index of the ContractEvent. 118 | * 119 | * @returns The event index. 120 | */ 121 | public eventIndex(): number { 122 | return this.model.event_index; 123 | } 124 | 125 | /** 126 | * Returns the event data of the ContractEvent. 127 | * 128 | * @returns The event data. 129 | */ 130 | public data(): string { 131 | return this.model.data; 132 | } 133 | 134 | /** 135 | * Print the ContractEvent as a string. 136 | * 137 | * @returns The string representation of the ContractEvent. 138 | */ 139 | public toString(): string { 140 | return `ContractEvent { networkId: '${this.networkId()}' protocolName: '${this.protocolName()}' contractName: '${this.contractName()}' eventName: '${this.eventName()}' contractAddress: '${this.contractAddress()}' blockHeight: ${this.blockHeight()} txHash: '${this.txHash()}' }`; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/coinbase/crypto_amount.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { CryptoAmount as CryptoAmountModel } from "../client/api"; 3 | import { Asset } from "./asset"; 4 | 5 | /** 6 | * A representation of a CryptoAmount that includes the amount and asset. 7 | */ 8 | export class CryptoAmount { 9 | private amount: Decimal; 10 | private assetObj: Asset; 11 | private assetId: string; 12 | 13 | /** 14 | * Creates a new CryptoAmount instance. 15 | * 16 | * @param amount - The amount of the Asset 17 | * @param asset - The Asset 18 | * @param assetId - Optional Asset ID override 19 | */ 20 | constructor(amount: Decimal, asset: Asset, assetId?: string) { 21 | this.amount = amount; 22 | this.assetObj = asset; 23 | this.assetId = assetId || asset.getAssetId(); 24 | } 25 | 26 | /** 27 | * Converts a CryptoAmount model to a CryptoAmount. 28 | * 29 | * @param amountModel - The crypto amount from the API 30 | * @returns The converted CryptoAmount object 31 | */ 32 | public static fromModel(amountModel: CryptoAmountModel): CryptoAmount { 33 | const asset = Asset.fromModel(amountModel.asset); 34 | return new CryptoAmount(asset.fromAtomicAmount(new Decimal(amountModel.amount)), asset); 35 | } 36 | 37 | /** 38 | * Converts a CryptoAmount model and asset ID to a CryptoAmount. 39 | * This can be used to specify a non-primary denomination that we want the amount 40 | * to be converted to. 41 | * 42 | * @param amountModel - The crypto amount from the API 43 | * @param assetId - The Asset ID of the denomination we want returned 44 | * @returns The converted CryptoAmount object 45 | */ 46 | public static fromModelAndAssetId(amountModel: CryptoAmountModel, assetId: string): CryptoAmount { 47 | const asset = Asset.fromModel(amountModel.asset, assetId); 48 | return new CryptoAmount( 49 | asset.fromAtomicAmount(new Decimal(amountModel.amount)), 50 | asset, 51 | assetId, 52 | ); 53 | } 54 | 55 | /** 56 | * Gets the amount of the Asset. 57 | * 58 | * @returns The amount of the Asset 59 | */ 60 | public getAmount(): Decimal { 61 | return this.amount; 62 | } 63 | 64 | /** 65 | * Gets the Asset. 66 | * 67 | * @returns The Asset 68 | */ 69 | public getAsset(): Asset { 70 | return this.assetObj; 71 | } 72 | 73 | /** 74 | * Gets the Asset ID. 75 | * 76 | * @returns The Asset ID 77 | */ 78 | public getAssetId(): string { 79 | return this.assetId; 80 | } 81 | 82 | /** 83 | * Converts the amount to atomic units. 84 | * 85 | * @returns The amount in atomic units 86 | */ 87 | public toAtomicAmount(): bigint { 88 | return this.assetObj.toAtomicAmount(this.amount); 89 | } 90 | 91 | /** 92 | * Returns a string representation of the CryptoAmount. 93 | * 94 | * @returns A string representation of the CryptoAmount 95 | */ 96 | public toString(): string { 97 | return `CryptoAmount{amount: '${this.amount}', assetId: '${this.assetId}'}`; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/coinbase/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * InvalidAPIKeyFormatError error is thrown when the API key format is invalid. 3 | */ 4 | export class InvalidAPIKeyFormatError extends Error { 5 | static DEFAULT_MESSAGE = "Invalid API key format"; 6 | 7 | /** 8 | * Initializes a new InvalidAPIKeyFormat instance. 9 | * 10 | * @param message - The error message. 11 | */ 12 | constructor(message: string = InvalidAPIKeyFormatError.DEFAULT_MESSAGE) { 13 | super(message); 14 | this.name = "InvalidAPIKeyFormatError"; 15 | if (Error.captureStackTrace) { 16 | Error.captureStackTrace(this, InvalidAPIKeyFormatError); 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * TimeoutError is thrown when an operation times out. 23 | */ 24 | export class TimeoutError extends Error { 25 | /** 26 | * Initializes a new TimeoutError instance. 27 | * 28 | * @param message - The error message. 29 | */ 30 | constructor(message: string = "Timeout Error") { 31 | super(message); 32 | this.name = "TimeoutError"; 33 | if (Error.captureStackTrace) { 34 | Error.captureStackTrace(this, TimeoutError); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * ArgumentError is thrown when an argument is invalid. 41 | */ 42 | export class ArgumentError extends Error { 43 | static DEFAULT_MESSAGE = "Argument Error"; 44 | 45 | /** 46 | * Initializes a new ArgumentError instance. 47 | * 48 | * @param message - The error message. 49 | */ 50 | constructor(message: string = ArgumentError.DEFAULT_MESSAGE) { 51 | super(message); 52 | this.name = "ArgumentError"; 53 | if (Error.captureStackTrace) { 54 | Error.captureStackTrace(this, ArgumentError); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * InvalidConfigurationError error is thrown when apikey/privateKey configuration is invalid. 61 | */ 62 | export class InvalidConfigurationError extends Error { 63 | static DEFAULT_MESSAGE = "Invalid configuration"; 64 | 65 | /** 66 | * Initializes a new InvalidConfiguration instance. 67 | * 68 | * @param message - The error message. 69 | */ 70 | constructor(message: string = InvalidConfigurationError.DEFAULT_MESSAGE) { 71 | super(message); 72 | this.name = "InvalidConfigurationError"; 73 | if (Error.captureStackTrace) { 74 | Error.captureStackTrace(this, InvalidConfigurationError); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * InvalidUnsignedPayload error is thrown when the unsigned payload is invalid. 81 | */ 82 | export class InvalidUnsignedPayloadError extends Error { 83 | static DEFAULT_MESSAGE = "Invalid unsigned payload"; 84 | 85 | /** 86 | * Initializes a new InvalidUnsignedPayload instance. 87 | * 88 | * @param message - The error message. 89 | */ 90 | constructor(message: string = InvalidUnsignedPayloadError.DEFAULT_MESSAGE) { 91 | super(message); 92 | this.name = "InvalidUnsignedPayloadError"; 93 | if (Error.captureStackTrace) { 94 | Error.captureStackTrace(this, InvalidUnsignedPayloadError); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * NotSignedError is thrown when a resource is not signed. 101 | */ 102 | export class NotSignedError extends Error { 103 | /** 104 | * Initializes a new NotSignedError instance. 105 | * 106 | * @param message - The error message. 107 | */ 108 | constructor(message: string = "Resource not signed") { 109 | super(message); 110 | this.name = "NotSignedError"; 111 | if (Error.captureStackTrace) { 112 | Error.captureStackTrace(this, NotSignedError); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * AlreadySignedError is thrown when a resource is already signed. 119 | */ 120 | export class AlreadySignedError extends Error { 121 | static DEFAULT_MESSAGE = "Resource already signed"; 122 | 123 | /** 124 | * Initializes a new AlreadySignedError instance. 125 | * 126 | * @param message - The error message. 127 | */ 128 | constructor(message: string = AlreadySignedError.DEFAULT_MESSAGE) { 129 | super(message); 130 | this.name = "AlreadySignedError"; 131 | if (Error.captureStackTrace) { 132 | Error.captureStackTrace(this, AlreadySignedError); 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * UninitializedSDKError is thrown when the Coinbase instance is not initialized. 139 | */ 140 | export class UninitializedSDKError extends Error { 141 | static DEFAULT_MESSAGE = 142 | "Coinbase SDK has not been initialized. Please initialize by calling either:\n\n" + 143 | "- Coinbase.configure({apiKeyName: '...', privateKey: '...'})\n" + 144 | "- Coinbase.configureFromJson({filePath: '/path/to/api_keys.json'})\n\n" + 145 | "If needed, register for API keys at https://portal.cdp.coinbase.com/ or view the docs at https://docs.cdp.coinbase.com/wallet-api/docs/welcome"; 146 | 147 | /** 148 | * Initializes a new UninitializedSDKError instance. 149 | * 150 | * @param message - The error message. 151 | */ 152 | constructor(message: string = UninitializedSDKError.DEFAULT_MESSAGE) { 153 | super(message); 154 | this.name = "UninitializedSDKError"; 155 | if (Error.captureStackTrace) { 156 | Error.captureStackTrace(this, UninitializedSDKError); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/coinbase/faucet_transaction.ts: -------------------------------------------------------------------------------- 1 | import { FaucetTransaction as FaucetTransactionModel } from "../client"; 2 | import { TransactionStatus } from "./types"; 3 | import { Coinbase } from "./coinbase"; 4 | import { Transaction } from "./transaction"; 5 | import { delay } from "./utils"; 6 | import { TimeoutError } from "./errors"; 7 | 8 | /** 9 | * Represents a transaction from a faucet. 10 | */ 11 | export class FaucetTransaction { 12 | private model: FaucetTransactionModel; 13 | private _transaction: Transaction; 14 | 15 | /** 16 | * Creates a new FaucetTransaction instance. 17 | * Do not use this method directly - instead, use Address.faucet(). 18 | * 19 | * @class 20 | * @param {FaucetTransactionModel} model - The FaucetTransaction model. 21 | * @throws {Error} If the model does not exist. 22 | */ 23 | constructor(model: FaucetTransactionModel) { 24 | if (!model?.transaction) { 25 | throw new Error("FaucetTransaction model cannot be empty"); 26 | } 27 | 28 | this.model = model; 29 | this._transaction = new Transaction(this.model.transaction!); 30 | } 31 | 32 | /** 33 | * Returns the Transaction of the FaucetTransaction. 34 | * 35 | * @returns The Faucet Transaction 36 | */ 37 | public get transaction(): Transaction { 38 | return this._transaction; 39 | } 40 | 41 | /** 42 | * Returns the transaction hash. 43 | * 44 | * @returns {string} The transaction hash. 45 | */ 46 | public getTransactionHash(): string { 47 | return this.transaction.getTransactionHash()!; 48 | } 49 | 50 | /** 51 | * Returns the link to the transaction on the blockchain explorer. 52 | * 53 | * @returns {string} The link to the transaction on the blockchain explorer 54 | */ 55 | public getTransactionLink(): string { 56 | return this.transaction.getTransactionLink()!; 57 | } 58 | 59 | /** 60 | * Returns the Status of the FaucetTransaction. 61 | * 62 | * @returns The Status of the FaucetTransaction. 63 | */ 64 | public getStatus(): TransactionStatus { 65 | return this.transaction.getStatus(); 66 | } 67 | 68 | /** 69 | * Returns the network ID of the FaucetTransaction. 70 | * 71 | * @returns {string} The network ID. 72 | */ 73 | public getNetworkId(): string { 74 | return this.transaction.getNetworkId(); 75 | } 76 | 77 | /** 78 | * Returns the address that is being funded by the faucet. 79 | * 80 | * @returns {string} The address ID. 81 | */ 82 | public getAddressId(): string { 83 | return this.transaction.toAddressId()!; 84 | } 85 | 86 | /** 87 | * Waits for the FaucetTransaction to be confirmed on the Network or fail on chain. 88 | * Waits until the FaucetTransaction is completed or failed on-chain by polling at the given interval. 89 | * Raises an error if the FaucetTransaction takes longer than the given timeout. 90 | * 91 | * @param options - The options to configure the wait function. 92 | * @param options.intervalSeconds - The interval to check the status of the FaucetTransaction. 93 | * @param options.timeoutSeconds - The maximum time to wait for the FaucetTransaction to be confirmed. 94 | * 95 | * @returns The FaucetTransaction object in a terminal state. 96 | * @throws {Error} if the FaucetTransaction times out. 97 | */ 98 | public async wait({ 99 | intervalSeconds = 0.2, 100 | timeoutSeconds = 10, 101 | } = {}): Promise { 102 | const startTime = Date.now(); 103 | 104 | while (Date.now() - startTime < timeoutSeconds * 1000) { 105 | await this.reload(); 106 | 107 | // If the FaucetTransaction is in a terminal state, return the FaucetTransaction. 108 | if (this.transaction.isTerminalState()) { 109 | return this; 110 | } 111 | 112 | await delay(intervalSeconds); 113 | } 114 | 115 | throw new TimeoutError("FaucetTransaction timed out"); 116 | } 117 | 118 | /** 119 | * Reloads the FaucetTransaction model with the latest data from the server. 120 | * 121 | * @returns {FaucetTransaction} The reloaded FaucetTransaction object. 122 | * @throws {APIError} if the API request to get a FaucetTransaction fails. 123 | */ 124 | public async reload(): Promise { 125 | const result = await Coinbase.apiClients.externalAddress!.getFaucetTransaction( 126 | this.transaction.getNetworkId(), 127 | this.getAddressId(), 128 | this.getTransactionHash(), 129 | ); 130 | 131 | this.model = result?.data; 132 | 133 | if (!this.model?.transaction) { 134 | throw new Error("FaucetTransaction model cannot be empty"); 135 | } 136 | 137 | this._transaction = new Transaction(this.model.transaction!); 138 | 139 | return this; 140 | } 141 | 142 | /** 143 | * Returns a string representation of the FaucetTransaction. 144 | * 145 | * @returns {string} A string representation of the FaucetTransaction. 146 | */ 147 | public toString(): string { 148 | return `Coinbase::FaucetTransaction{transaction_hash: '${this.getTransactionHash()}', transaction_link: '${this.getTransactionLink()}'}`; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/coinbase/fiat_amount.ts: -------------------------------------------------------------------------------- 1 | import { FiatAmount as FiatAmountModel } from "../client/api"; 2 | 3 | /** 4 | * A representation of a FiatAmount that includes the amount and currency. 5 | */ 6 | export class FiatAmount { 7 | private amount: string; 8 | private currency: string; 9 | 10 | /** 11 | * Initialize a new FiatAmount. Do not use this directly, use the fromModel method instead. 12 | * 13 | * @param amount - The amount in the fiat currency 14 | * @param currency - The currency code (e.g. 'USD') 15 | */ 16 | constructor(amount: string, currency: string) { 17 | this.amount = amount; 18 | this.currency = currency; 19 | } 20 | 21 | /** 22 | * Convert a FiatAmount model to a FiatAmount. 23 | * 24 | * @param fiatAmountModel - The fiat amount from the API. 25 | * @returns The converted FiatAmount object. 26 | */ 27 | public static fromModel(fiatAmountModel: FiatAmountModel): FiatAmount { 28 | return new FiatAmount(fiatAmountModel.amount, fiatAmountModel.currency); 29 | } 30 | 31 | /** 32 | * Get the amount in the fiat currency. 33 | * 34 | * @returns The amount in the fiat currency. 35 | */ 36 | public getAmount(): string { 37 | return this.amount; 38 | } 39 | 40 | /** 41 | * Get the currency code. 42 | * 43 | * @returns The currency code. 44 | */ 45 | public getCurrency(): string { 46 | return this.currency; 47 | } 48 | 49 | /** 50 | * Get a string representation of the FiatAmount. 51 | * 52 | * @returns A string representation of the FiatAmount. 53 | */ 54 | public toString(): string { 55 | return `FiatAmount(amount: '${this.amount}', currency: '${this.currency}')`; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/coinbase/fund_quote.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from "decimal.js"; 2 | import { FundQuote as FundQuoteModel } from "../client/api"; 3 | import { Asset } from "./asset"; 4 | import { CryptoAmount } from "./crypto_amount"; 5 | import { Coinbase } from "./coinbase"; 6 | import { FundOperation } from "./fund_operation"; 7 | 8 | /** 9 | * A representation of a Fund Operation Quote. 10 | */ 11 | export class FundQuote { 12 | private model: FundQuoteModel; 13 | private asset: Asset | null = null; 14 | 15 | /** 16 | * Creates a new FundQuote instance. 17 | * 18 | * @param model - The model representing the fund quote 19 | */ 20 | constructor(model: FundQuoteModel) { 21 | this.model = model; 22 | } 23 | 24 | /** 25 | * Converts a FundQuoteModel into a FundQuote object. 26 | * 27 | * @param fundQuoteModel - The FundQuote model object. 28 | * @returns The FundQuote object. 29 | */ 30 | public static fromModel(fundQuoteModel: FundQuoteModel): FundQuote { 31 | return new FundQuote(fundQuoteModel); 32 | } 33 | 34 | /** 35 | * Create a new Fund Operation Quote. 36 | * 37 | * @param walletId - The Wallet ID 38 | * @param addressId - The Address ID 39 | * @param amount - The amount of the Asset 40 | * @param assetId - The Asset ID 41 | * @param networkId - The Network ID 42 | * @returns The new FundQuote object 43 | */ 44 | public static async create( 45 | walletId: string, 46 | addressId: string, 47 | amount: Decimal, 48 | assetId: string, 49 | networkId: string, 50 | ): Promise { 51 | const asset = await Asset.fetch(networkId, assetId); 52 | 53 | const response = await Coinbase.apiClients.fund!.createFundQuote(walletId, addressId, { 54 | asset_id: Asset.primaryDenomination(assetId), 55 | amount: asset.toAtomicAmount(amount).toString(), 56 | }); 57 | 58 | return FundQuote.fromModel(response.data); 59 | } 60 | 61 | /** 62 | * Gets the Fund Quote ID. 63 | * 64 | * @returns {string} The unique identifier of the fund quote 65 | */ 66 | public getId(): string { 67 | return this.model.fund_quote_id; 68 | } 69 | 70 | /** 71 | * Gets the Network ID. 72 | * 73 | * @returns {string} The network identifier 74 | */ 75 | public getNetworkId(): string { 76 | return this.model.network_id; 77 | } 78 | 79 | /** 80 | * Gets the Wallet ID. 81 | * 82 | * @returns {string} The wallet identifier 83 | */ 84 | public getWalletId(): string { 85 | return this.model.wallet_id; 86 | } 87 | 88 | /** 89 | * Gets the Address ID. 90 | * 91 | * @returns {string} The address identifier 92 | */ 93 | public getAddressId(): string { 94 | return this.model.address_id; 95 | } 96 | 97 | /** 98 | * Gets the Asset. 99 | * 100 | * @returns {Asset} The asset associated with this quote 101 | */ 102 | public getAsset(): Asset { 103 | if (!this.asset) { 104 | this.asset = Asset.fromModel(this.model.crypto_amount.asset); 105 | } 106 | return this.asset; 107 | } 108 | 109 | /** 110 | * Gets the crypto amount. 111 | * 112 | * @returns {CryptoAmount} The cryptocurrency amount 113 | */ 114 | public getAmount(): CryptoAmount { 115 | return CryptoAmount.fromModel(this.model.crypto_amount); 116 | } 117 | 118 | /** 119 | * Gets the fiat amount. 120 | * 121 | * @returns {Decimal} The fiat amount in decimal format 122 | */ 123 | public getFiatAmount(): Decimal { 124 | return new Decimal(this.model.fiat_amount.amount); 125 | } 126 | 127 | /** 128 | * Gets the fiat currency. 129 | * 130 | * @returns {string} The fiat currency code 131 | */ 132 | public getFiatCurrency(): string { 133 | return this.model.fiat_amount.currency; 134 | } 135 | 136 | /** 137 | * Gets the buy fee. 138 | * 139 | * @returns {{ amount: string; currency: string }} The buy fee amount and currency 140 | */ 141 | public getBuyFee(): { amount: string; currency: string } { 142 | return { 143 | amount: this.model.fees.buy_fee.amount, 144 | currency: this.model.fees.buy_fee.currency, 145 | }; 146 | } 147 | 148 | /** 149 | * Gets the transfer fee. 150 | * 151 | * @returns {CryptoAmount} The transfer fee as a crypto amount 152 | */ 153 | public getTransferFee(): CryptoAmount { 154 | return CryptoAmount.fromModel(this.model.fees.transfer_fee); 155 | } 156 | 157 | /** 158 | * Execute the fund quote to create a fund operation. 159 | * 160 | * @returns {Promise} A promise that resolves to the created fund operation 161 | */ 162 | public async execute(): Promise { 163 | return FundOperation.create( 164 | this.getWalletId(), 165 | this.getAddressId(), 166 | this.getAmount().getAmount(), 167 | this.getAsset().getAssetId(), 168 | this.getNetworkId(), 169 | this, 170 | ); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/coinbase/hash.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { TypedDataDomain, TypedDataField } from "./types"; 3 | 4 | /** 5 | * Computes the EIP-191 personal-sign message digest to sign. 6 | * 7 | * @returns The EIP-191 hash of the message as a string. 8 | * @throws {Error} if the message cannot be hashed. 9 | * @param message - The message to hash. 10 | */ 11 | export const hashMessage = (message: Uint8Array | string): string => { 12 | return ethers.hashMessage(message); 13 | }; 14 | 15 | /** 16 | * Computes the hash of the EIP-712 compliant typed data message. 17 | * 18 | * @param domain - The domain parameters for the EIP-712 message, including the name, version, chainId, and verifying contract. 19 | * @param types - The types definitions for the EIP-712 message, represented as a record of type names to their fields. 20 | * @param value - The actual data object to hash, conforming to the types defined. 21 | * 22 | * @returns The EIP-712 hash of the typed data as a hex-encoded string. 23 | * @throws {Error} if the typed data cannot be hashed. 24 | */ 25 | export const hashTypedDataMessage = ( 26 | domain: TypedDataDomain, 27 | types: Record>, 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | value: Record, 30 | ): string => { 31 | return ethers.TypedDataEncoder.hash(domain, types, value); 32 | }; 33 | -------------------------------------------------------------------------------- /src/coinbase/historical_balance.ts: -------------------------------------------------------------------------------- 1 | import Decimal from "decimal.js"; 2 | import { HistoricalBalance as HistoricalBalanceModel } from "../client"; 3 | import { Asset } from "./asset"; 4 | 5 | /** A representation of historical balance. */ 6 | export class HistoricalBalance { 7 | public readonly amount: Decimal; 8 | public readonly blockHash: string; 9 | public readonly blockHeight: Decimal; 10 | public readonly asset: Asset; 11 | 12 | /** 13 | * Private constructor to prevent direct instantiation outside of the factory methods. 14 | * 15 | * @ignore 16 | * @param {Decimal} amount - The amount of the balance. 17 | * @param {Decimal} blockHeight - The block height at which the balance was recorded. 18 | * @param {string} blockHash - The block hash at which the balance was recorded 19 | * @param {string} asset - The asset we want to fetch. 20 | * @hideconstructor 21 | */ 22 | private constructor(amount: Decimal, blockHeight: Decimal, blockHash: string, asset: Asset) { 23 | this.amount = amount; 24 | this.blockHeight = blockHeight; 25 | this.blockHash = blockHash; 26 | this.asset = asset; 27 | } 28 | 29 | /** 30 | * Converts a HistoricalBalanceModel into a HistoricalBalance object. 31 | * 32 | * @param {HistoricalBalanceModel} model - The historical balance model object. 33 | * @returns {HistoricalBalance} The HistoricalBalance object. 34 | */ 35 | public static fromModel(model: HistoricalBalanceModel): HistoricalBalance { 36 | const asset = Asset.fromModel(model.asset); 37 | return new HistoricalBalance( 38 | asset.fromAtomicAmount(new Decimal(model.amount)), 39 | new Decimal(model.block_height), 40 | model.block_hash, 41 | asset, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/coinbase/payload_signature.ts: -------------------------------------------------------------------------------- 1 | import { PayloadSignature as PayloadSignatureModel } from "../client"; 2 | import { PayloadSignatureStatus } from "./types"; 3 | import { delay } from "./utils"; 4 | import { TimeoutError } from "./errors"; 5 | import { Coinbase } from "./coinbase"; 6 | 7 | /** 8 | * A representation of a Payload Signature. 9 | */ 10 | export class PayloadSignature { 11 | private model: PayloadSignatureModel; 12 | 13 | /** 14 | * Constructs a Payload Signature. 15 | * 16 | * @class 17 | * @param model - The underlying Payload Signature object. 18 | */ 19 | constructor(model: PayloadSignatureModel) { 20 | if (!model) { 21 | throw new Error("Invalid model type"); 22 | } 23 | this.model = model; 24 | } 25 | 26 | /** 27 | * Returns the ID of the Payload Signature. 28 | * 29 | * @returns The ID of the Payload Signature 30 | */ 31 | getId(): string { 32 | return this.model.payload_signature_id; 33 | } 34 | 35 | /** 36 | * Returns the Wallet ID of the Payload Signature. 37 | * 38 | * @returns The Wallet ID 39 | */ 40 | getWalletId(): string { 41 | return this.model.wallet_id; 42 | } 43 | 44 | /** 45 | * Returns the Address ID of the Payload Signature. 46 | * 47 | * @returns The Address ID 48 | */ 49 | getAddressId(): string { 50 | return this.model.address_id; 51 | } 52 | 53 | /** 54 | * Returns the Unsigned Payload of the Payload Signature. 55 | * 56 | * @returns The Unsigned Payload 57 | */ 58 | getUnsignedPayload(): string { 59 | return this.model.unsigned_payload; 60 | } 61 | 62 | /** 63 | * Returns the Signature of the Payload Signature. 64 | * 65 | * @returns The Signature 66 | */ 67 | getSignature(): string | undefined { 68 | return this.model.signature; 69 | } 70 | 71 | /** 72 | * Returns the Status of the Payload Signature. 73 | * 74 | * @returns The Status 75 | */ 76 | getStatus(): PayloadSignatureStatus | undefined { 77 | switch (this.model.status) { 78 | case PayloadSignatureStatus.PENDING: 79 | return PayloadSignatureStatus.PENDING; 80 | case PayloadSignatureStatus.SIGNED: 81 | return PayloadSignatureStatus.SIGNED; 82 | case PayloadSignatureStatus.FAILED: 83 | return PayloadSignatureStatus.FAILED; 84 | default: 85 | return undefined; 86 | } 87 | } 88 | 89 | /** 90 | * Returns whether the Payload Signature is in a terminal State. 91 | * 92 | * @returns Whether the Payload Signature is in a terminal State 93 | */ 94 | isTerminalState(): boolean { 95 | const status = this.getStatus(); 96 | 97 | if (!status) return false; 98 | 99 | return [PayloadSignatureStatus.SIGNED, PayloadSignatureStatus.FAILED].includes(status); 100 | } 101 | 102 | /** 103 | * Waits for the Payload Signature to be signed or for the signature operation to fail. 104 | * 105 | * @param options - The options to configure the wait function. 106 | * @param options.intervalSeconds - The interval to check the status of the Payload Signature. 107 | * @param options.timeoutSeconds - The maximum time to wait for the Payload Signature to be confirmed. 108 | * 109 | * @returns The Payload Signature object in a terminal state. 110 | * @throws {Error} if the Payload Signature times out. 111 | */ 112 | public async wait({ 113 | intervalSeconds = 0.2, 114 | timeoutSeconds = 10, 115 | } = {}): Promise { 116 | const startTime = Date.now(); 117 | 118 | while (Date.now() - startTime < timeoutSeconds * 1000) { 119 | await this.reload(); 120 | 121 | // If the Payload Signature is in a terminal state, return the Payload Signature. 122 | if (this.isTerminalState()) { 123 | return this; 124 | } 125 | 126 | await delay(intervalSeconds); 127 | } 128 | 129 | throw new TimeoutError("Payload Signature timed out"); 130 | } 131 | 132 | /** 133 | * Reloads the Payload Signature model with the latest data from the server. 134 | * 135 | * @throws {APIError} if the API request to get a Payload Signature fails. 136 | */ 137 | public async reload(): Promise { 138 | const result = await Coinbase.apiClients.address!.getPayloadSignature( 139 | this.getWalletId(), 140 | this.getAddressId(), 141 | this.getId(), 142 | ); 143 | this.model = result?.data; 144 | } 145 | 146 | /** 147 | * Returns a string representation of the Payload Signature. 148 | * 149 | * @returns A string representation of the Payload Signature. 150 | */ 151 | toString(): string { 152 | return `PayloadSignature { status: '${this.getStatus()}', unsignedPayload: '${this.getUnsignedPayload()}', signature: ${this.getSignature()} }`; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/coinbase/read_contract.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Abi } from "abitype"; 3 | import { Coinbase } from "./coinbase"; 4 | import { ContractFunctionName } from "viem"; 5 | import { SolidityValue } from "../client"; 6 | import { ContractFunctionReturnType } from "./types/contract"; 7 | 8 | /** 9 | * Converts a SolidityValue to its corresponding JavaScript type. 10 | * 11 | * @param {SolidityValue} solidityValue - The Solidity value to convert. 12 | * @returns {unknown} The converted JavaScript value. 13 | */ 14 | function convertSolidityValue(solidityValue: SolidityValue): unknown { 15 | const { type, value, values } = solidityValue; 16 | 17 | switch (type) { 18 | case "uint8": 19 | case "uint16": 20 | case "uint32": 21 | case "int8": 22 | case "int16": 23 | case "int32": 24 | return Number(value); 25 | case "uint64": 26 | case "uint128": 27 | case "uint256": 28 | case "int64": 29 | case "int128": 30 | case "int256": 31 | return BigInt(value!); 32 | case "address": 33 | return value as `0x${string}`; 34 | case "bool": 35 | return value === "true"; 36 | case "string": 37 | return value; 38 | case "bytes": 39 | case "bytes1": 40 | case "bytes2": 41 | case "bytes3": 42 | case "bytes4": 43 | case "bytes5": 44 | case "bytes6": 45 | case "bytes7": 46 | case "bytes8": 47 | case "bytes9": 48 | case "bytes10": 49 | case "bytes11": 50 | case "bytes12": 51 | case "bytes13": 52 | case "bytes14": 53 | case "bytes15": 54 | case "bytes16": 55 | case "bytes17": 56 | case "bytes18": 57 | case "bytes19": 58 | case "bytes20": 59 | case "bytes21": 60 | case "bytes22": 61 | case "bytes23": 62 | case "bytes24": 63 | case "bytes25": 64 | case "bytes26": 65 | case "bytes27": 66 | case "bytes28": 67 | case "bytes29": 68 | case "bytes30": 69 | case "bytes31": 70 | case "bytes32": 71 | return value as `0x${string}`; 72 | case "array": 73 | return values!.map(convertSolidityValue); 74 | case "tuple": 75 | return values!.reduce( 76 | (acc, val) => { 77 | if (!val.name) { 78 | throw new Error("Tuple field missing name"); 79 | } 80 | acc[val.name] = convertSolidityValue(val); 81 | return acc; 82 | }, 83 | {} as Record, 84 | ); 85 | default: 86 | throw new Error(`Unsupported Solidity type: ${type}`); 87 | } 88 | } 89 | 90 | /** 91 | * Parses a SolidityValue to a specific type T. 92 | * 93 | * @template T 94 | * @param {SolidityValue} solidityValue - The Solidity value to parse. 95 | * @returns {T} The parsed value of type T. 96 | */ 97 | function parseSolidityValue(solidityValue: SolidityValue): T { 98 | return convertSolidityValue(solidityValue) as T; 99 | } 100 | 101 | /** 102 | * Reads data from a smart contract using the Coinbase API. 103 | * 104 | * @template TAbi - The ABI type. 105 | * @template TFunctionName - The contract function name type. 106 | * @template TArgs - The function arguments type. 107 | * @param {object} params - The parameters for reading the contract. 108 | * @param {string} params.networkId - The network ID. 109 | * @param {string} params.contractAddress - The contract address (as a hexadecimal string). 110 | * @param {TFunctionName} params.method - The contract method to call. 111 | * @param {TArgs} params.args - The arguments for the contract method. 112 | * @param {TAbi} [params.abi] - The contract ABI (optional). 113 | * @returns {Promise} The result of the contract call. 114 | */ 115 | export async function readContract< 116 | TAbi extends Abi | undefined, 117 | TFunctionName extends TAbi extends Abi ? ContractFunctionName : string, 118 | TArgs extends Record, 119 | >(params: { 120 | networkId: string; 121 | contractAddress: `0x${string}`; 122 | method: TFunctionName; 123 | args: TArgs; 124 | abi?: TAbi; 125 | }): Promise< 126 | TAbi extends Abi 127 | ? ContractFunctionReturnType< 128 | TAbi, 129 | Extract>, 130 | TArgs 131 | > 132 | : any 133 | > { 134 | const response = await Coinbase.apiClients.smartContract!.readContract( 135 | params.networkId, 136 | params.contractAddress, 137 | { 138 | method: params.method, 139 | args: JSON.stringify(params.args || {}), 140 | abi: params.abi ? JSON.stringify(params.abi) : undefined, 141 | }, 142 | ); 143 | 144 | return parseSolidityValue< 145 | TAbi extends Abi 146 | ? ContractFunctionReturnType< 147 | TAbi, 148 | Extract>, 149 | TArgs 150 | > 151 | : any 152 | >(response.data); 153 | } 154 | -------------------------------------------------------------------------------- /src/coinbase/server_signer.ts: -------------------------------------------------------------------------------- 1 | import { Coinbase } from "./coinbase"; 2 | import { ServerSigner as ServerSignerModel } from "../client/api"; 3 | 4 | /** 5 | * A representation of a Server-Signer. Server-Signers are assigned to sign transactions for a Wallet. 6 | */ 7 | export class ServerSigner { 8 | private model: ServerSignerModel; 9 | 10 | /** 11 | * Private constructor to prevent direct instantiation outside of factory method. 12 | * Creates a new ServerSigner instance. 13 | * Do not use this method directly. Instead, use ServerSigner.getDefault(). 14 | * 15 | * @ignore 16 | * @param serverSignerModel - The Server-Signer model. 17 | * @hideconstructor 18 | */ 19 | private constructor(serverSignerModel: ServerSignerModel) { 20 | this.model = serverSignerModel; 21 | } 22 | 23 | /** 24 | * Returns the default Server-Signer for the CDP Project. 25 | * 26 | * @returns The default Server-Signer. 27 | * @throws {APIError} if the API request to list Server-Signers fails. 28 | * @throws {Error} if there is no Server-Signer associated with the CDP Project. 29 | */ 30 | public static async getDefault(): Promise { 31 | const response = await Coinbase.apiClients.serverSigner!.listServerSigners(); 32 | if (response.data.data.length === 0) { 33 | throw new Error("No Server-Signer is associated with the project"); 34 | } 35 | 36 | return new ServerSigner(response.data.data[0]); 37 | } 38 | 39 | /** 40 | * Returns the ID of the Server-Signer. 41 | * 42 | * @returns The Server-Signer ID. 43 | */ 44 | public getId(): string { 45 | return this.model.server_signer_id; 46 | } 47 | 48 | /** 49 | * Returns the IDs of the Wallet's the Server-Signer can sign for. 50 | * 51 | * @returns The Wallet IDs. 52 | */ 53 | public getWallets(): string[] | undefined { 54 | return this.model.wallets; 55 | } 56 | 57 | /** 58 | * Returns a String representation of the Server-Signer. 59 | * 60 | * @returns a String representation of the Server-Signer. 61 | */ 62 | public toString(): string { 63 | return `ServerSigner{id: '${this.getId()}', wallets: '${this.getWallets()}'}`; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/coinbase/sponsored_send.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { SponsoredSend as SponsoredSendModel } from "../client/api"; 3 | import { SponsoredSendStatus } from "./types"; 4 | 5 | /** 6 | * A representation of an onchain Sponsored Send. 7 | */ 8 | export class SponsoredSend { 9 | private model: SponsoredSendModel; 10 | 11 | /** 12 | * Sponsored Sends should be constructed via higher level abstractions like Transfer. 13 | * 14 | * @class 15 | * @param model - The underlying Sponsored Send object. 16 | */ 17 | constructor(model: SponsoredSendModel) { 18 | if (!model) { 19 | throw new Error("Invalid model type"); 20 | } 21 | this.model = model; 22 | } 23 | 24 | /** 25 | * Returns the Keccak256 hash of the typed data. This payload must be signed 26 | * by the sender to be used as an approval in the EIP-3009 transaction. 27 | * 28 | * @returns The Keccak256 hash of the typed data. 29 | */ 30 | getTypedDataHash(): string { 31 | return this.model.typed_data_hash; 32 | } 33 | 34 | /** 35 | * Returns the signature of the typed data. 36 | * 37 | * @returns The hash of the typed data signature. 38 | */ 39 | getSignature(): string | undefined { 40 | return this.model.signature; 41 | } 42 | 43 | /** 44 | * Signs the Sponsored Send with the provided key and returns the hex signature. 45 | * 46 | * @param key - The key to sign the Sponsored Send with 47 | * @returns The hex-encoded signature 48 | */ 49 | async sign(key: ethers.Wallet) { 50 | ethers.toBeArray; 51 | const signature = key.signingKey.sign(ethers.getBytes(this.getTypedDataHash())).serialized; 52 | this.model.signature = signature; 53 | return signature; 54 | } 55 | 56 | /** 57 | * Returns whether the Sponsored Send has been signed. 58 | * 59 | * @returns if the Sponsored Send has been signed. 60 | */ 61 | isSigned(): boolean { 62 | return !!this.getSignature(); 63 | } 64 | 65 | /** 66 | * Returns the Status of the Sponsored Send. 67 | * 68 | * @returns the Status of the Sponsored Send 69 | */ 70 | getStatus(): SponsoredSendStatus | undefined { 71 | switch (this.model.status) { 72 | case SponsoredSendStatus.PENDING: 73 | return SponsoredSendStatus.PENDING; 74 | case SponsoredSendStatus.SIGNED: 75 | return SponsoredSendStatus.SIGNED; 76 | case SponsoredSendStatus.SUBMITTED: 77 | return SponsoredSendStatus.SUBMITTED; 78 | case SponsoredSendStatus.COMPLETE: 79 | return SponsoredSendStatus.COMPLETE; 80 | case SponsoredSendStatus.FAILED: 81 | return SponsoredSendStatus.FAILED; 82 | default: 83 | undefined; 84 | } 85 | } 86 | 87 | /** 88 | * Returns whether the Sponsored Send is in a terminal State. 89 | * 90 | * @returns Whether the Sponsored Send is in a terminal State 91 | */ 92 | isTerminalState(): boolean { 93 | const status = this.getStatus(); 94 | 95 | if (!status) return false; 96 | 97 | return [SponsoredSendStatus.COMPLETE, SponsoredSendStatus.FAILED].includes(status); 98 | } 99 | 100 | /** 101 | * Returns the Transaction Hash of the Sponsored Send. 102 | * 103 | * @returns The Transaction Hash 104 | */ 105 | getTransactionHash(): string | undefined { 106 | return this.model.transaction_hash; 107 | } 108 | 109 | /** 110 | * Returns the link to the Sponsored Send on the blockchain explorer. 111 | * 112 | * @returns The link to the Sponsored Send on the blockchain explorer 113 | */ 114 | getTransactionLink(): string | undefined { 115 | return this.model.transaction_link; 116 | } 117 | 118 | /** 119 | * Returns a string representation of the Sponsored Send. 120 | * 121 | * @returns A string representation of the Sponsored Send 122 | */ 123 | toString(): string { 124 | return `SponsoredSend { transactionHash: '${this.getTransactionHash()}', status: '${this.getStatus()}', typedDataHash: '${this.getTypedDataHash()}', signature: ${this.getSignature()}, transactionLink: ${this.getTransactionLink()} }`; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/coinbase/staking_balance.ts: -------------------------------------------------------------------------------- 1 | import { StakingBalance as StakingBalanceModel } from "../client"; 2 | import { Balance } from "./balance"; 3 | import { Coinbase } from "./coinbase"; 4 | 5 | /** 6 | * A representation of the staking balance for a given asset on a specific date. 7 | */ 8 | export class StakingBalance { 9 | private model: StakingBalanceModel; 10 | 11 | /** 12 | * Creates the StakingBalance object. 13 | * 14 | * @param model - The underlying staking balance object. 15 | */ 16 | constructor(model: StakingBalanceModel) { 17 | this.model = model; 18 | } 19 | 20 | /** 21 | * Returns a list of StakingBalances for the provided network, asset, and address. 22 | * 23 | * @param networkId - The network ID. 24 | * @param assetId - The asset ID. 25 | * @param addressId - The address ID. 26 | * @param startTime - The start time. 27 | * @param endTime - The end time. 28 | * @returns The staking balances. 29 | */ 30 | public static async list( 31 | networkId: string, 32 | assetId: string, 33 | addressId: string, 34 | startTime: string, 35 | endTime: string, 36 | ): Promise { 37 | const stakingBalances: StakingBalance[] = []; 38 | const queue: string[] = [""]; 39 | 40 | while (queue.length > 0) { 41 | const page = queue.shift(); 42 | 43 | const response = await Coinbase.apiClients.stake!.fetchHistoricalStakingBalances( 44 | networkId, 45 | assetId, 46 | addressId, 47 | startTime, 48 | endTime, 49 | 100, 50 | page?.length ? page : undefined, 51 | ); 52 | 53 | response.data.data.forEach(stakingBalance => { 54 | stakingBalances.push(new StakingBalance(stakingBalance)); 55 | }); 56 | 57 | if (response.data.has_more) { 58 | if (response.data.next_page) { 59 | queue.push(response.data.next_page); 60 | } 61 | } 62 | } 63 | 64 | return stakingBalances; 65 | } 66 | 67 | /** 68 | * Returns the bonded stake amount of the StakingBalance. 69 | * 70 | * @returns The Balance. 71 | */ 72 | public bondedStake(): Balance { 73 | return Balance.fromModel(this.model.bonded_stake); 74 | } 75 | 76 | /** 77 | * Returns the unbonded stake amount of the StakingBalance. 78 | * 79 | * @returns The Balance. 80 | */ 81 | public unbondedBalance(): Balance { 82 | return Balance.fromModel(this.model.unbonded_balance); 83 | } 84 | 85 | /** 86 | * Returns the participant type of the address. 87 | * 88 | * @returns The participant type. 89 | */ 90 | public participantType(): string { 91 | return this.model.participant_type; 92 | } 93 | 94 | /** 95 | * Returns the date of the StakingBalance. 96 | * 97 | * @returns The date. 98 | */ 99 | public date(): Date { 100 | return new Date(this.model.date); 101 | } 102 | 103 | /** 104 | * Returns the onchain address of the StakingBalance. 105 | * 106 | * @returns The onchain address. 107 | */ 108 | public address(): string { 109 | return this.model.address; 110 | } 111 | 112 | /** 113 | * Print the Staking Balance as a string. 114 | * 115 | * @returns The string representation of the Staking Balance. 116 | */ 117 | public toString(): string { 118 | return `StakingBalance { date: '${this.date().toISOString()}' address: '${this.address()}' bondedStake: '${this.bondedStake().amount} ${this.bondedStake().asset?.assetId?.toUpperCase()}' unbondedBalance: '${this.unbondedBalance().amount} ${this.unbondedBalance().asset?.assetId?.toUpperCase()}' participantType: '${this.participantType()}' }`; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/coinbase/staking_reward.ts: -------------------------------------------------------------------------------- 1 | import { StakingReward as StakingRewardModel } from "../client"; 2 | import Decimal from "decimal.js"; 3 | import { Coinbase } from "./coinbase"; 4 | import { Asset } from "./asset"; 5 | import { Amount, StakingRewardFormat } from "./types"; 6 | 7 | /** 8 | * A representation of a staking reward earned on a network for a given asset. 9 | */ 10 | export class StakingReward { 11 | private model: StakingRewardModel; 12 | private asset: Asset; 13 | private readonly format: StakingRewardFormat; 14 | 15 | /** 16 | * Creates the StakingReward object. 17 | * 18 | * @param model - The underlying staking reward object. 19 | * @param asset - The asset for the staking reward. 20 | * @param format - The format to return the rewards in. (usd, native). Defaults to usd. 21 | */ 22 | constructor(model: StakingRewardModel, asset: Asset, format: StakingRewardFormat) { 23 | this.model = model; 24 | this.asset = asset; 25 | this.format = format; 26 | } 27 | 28 | /** 29 | * Returns a list of StakingRewards for the provided network, asset, and addresses. 30 | * 31 | * @param networkId - The network ID. 32 | * @param assetId - The asset ID. 33 | * @param addressIds - The address ID. 34 | * @param startTime - The start time. 35 | * @param endTime - The end time. 36 | * @param format - The format to return the rewards in. (usd, native). Defaults to usd. 37 | * @returns The staking rewards. 38 | */ 39 | public static async list( 40 | networkId: string, 41 | assetId: string, 42 | addressIds: Array, 43 | startTime: string, 44 | endTime: string, 45 | format: StakingRewardFormat = StakingRewardFormat.USD, 46 | ): Promise { 47 | const stakingRewards: StakingReward[] = []; 48 | const queue: string[] = [""]; 49 | 50 | while (queue.length > 0) { 51 | const page = queue.shift(); 52 | const request = { 53 | network_id: Coinbase.normalizeNetwork(networkId), 54 | asset_id: assetId, 55 | address_ids: addressIds, 56 | start_time: startTime, 57 | end_time: endTime, 58 | format: format, 59 | }; 60 | 61 | const response = await Coinbase.apiClients.stake!.fetchStakingRewards( 62 | request, 63 | 100, 64 | page?.length ? page : undefined, 65 | ); 66 | const asset = await Asset.fetch(networkId, assetId); 67 | 68 | response.data.data.forEach(stakingReward => { 69 | stakingRewards.push(new StakingReward(stakingReward, asset, format)); 70 | }); 71 | 72 | if (response.data.has_more) { 73 | if (response.data.next_page) { 74 | queue.push(response.data.next_page); 75 | } 76 | } 77 | } 78 | 79 | return stakingRewards; 80 | } 81 | 82 | /** 83 | * Returns the amount of the StakingReward. 84 | * 85 | * @returns The amount. 86 | */ 87 | public amount(): Amount { 88 | if (this.model.amount == "") return 0; 89 | if (this.format == StakingRewardFormat.USD) { 90 | return new Decimal(this.model.amount).div(new Decimal("100")); 91 | } 92 | return this.asset.fromAtomicAmount(new Decimal(this.model.amount)).toNumber(); 93 | } 94 | 95 | /** 96 | * Returns the date of the StakingReward. 97 | * 98 | * @returns The date. 99 | */ 100 | public date(): Date { 101 | return new Date(this.model.date); 102 | } 103 | 104 | /** 105 | * Returns the onchain address of the StakingReward. 106 | * 107 | * @returns The onchain address. 108 | */ 109 | public addressId(): string { 110 | return this.model.address_id; 111 | } 112 | 113 | /** 114 | * Returns the USD value of the StakingReward. 115 | * 116 | * @returns The USD value. 117 | */ 118 | public usdValue(): Amount { 119 | return new Decimal(this.model.usd_value.amount).div(new Decimal("100")); 120 | } 121 | 122 | /** 123 | * Returns the conversion price of the StakingReward in USD. 124 | * 125 | * @returns The conversion price. 126 | */ 127 | public conversionPrice(): Amount { 128 | return new Decimal(this.model.usd_value.conversion_price); 129 | } 130 | 131 | /** 132 | * Returns the time of calculating the conversion price. 133 | * 134 | * @returns The conversion time. 135 | */ 136 | public conversionTime(): Date { 137 | return new Date(this.model.usd_value.conversion_time); 138 | } 139 | 140 | /** 141 | * Print the Staking Reward as a string. 142 | * 143 | * @returns The string representation of the Staking Reward. 144 | */ 145 | public toString(): string { 146 | return `StakingReward { date: '${this.date().toISOString()}' address: '${this.addressId()}' amount: '${this.amount().toString()}' usd_value: '${this.usdValue().toString()}' conversion_price: '${this.conversionPrice().toString()}' conversion_time: '${this.conversionTime().toISOString()}' }`; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/coinbase/types/contract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | AbiFunction, 4 | AbiParameter, 5 | AbiParametersToPrimitiveTypes, 6 | ExtractAbiFunction, 7 | } from "abitype"; 8 | import { ContractFunctionName } from "viem"; 9 | 10 | /** 11 | * Converts an array of ABI parameters to a dictionary type. 12 | * Each parameter name becomes a key in the resulting dictionary, with a string value. 13 | */ 14 | type AbiParametersToDictionary = { 15 | [K in T[number]["name"] as K extends string ? K : any]: string; 16 | }; 17 | 18 | /** 19 | * Checks if two types are exactly the same. 20 | * Returns true if TArgs and TParams are identical, false otherwise. 21 | */ 22 | type MatchesParams = TArgs extends TParams 23 | ? TParams extends TArgs 24 | ? true 25 | : false 26 | : false; 27 | 28 | /** 29 | * Matches the provided arguments to a specific function in the ABI. 30 | * Returns the matched AbiFunction if found, never otherwise. 31 | */ 32 | type MatchArgsToFunction< 33 | TAbi extends Abi, 34 | TFunctionName extends string, 35 | TArgs extends Record, 36 | > = 37 | ExtractAbiFunction extends infer TFunctions 38 | ? TFunctions extends AbiFunction 39 | ? MatchesParams> extends true 40 | ? TFunctions 41 | : any 42 | : any 43 | : any; 44 | 45 | /** 46 | * Determines the return type of a contract function based on the ABI, function name, and arguments. 47 | * 48 | * @template TAbi - The ABI of the contract 49 | * @template TFunctionName - The name of the function to call 50 | * @template TArgs - The arguments to pass to the function (optional) 51 | * 52 | * @returns The return type of the function: 53 | * - void if the function has no outputs 54 | * - The single output type if there's only one output 55 | * - A tuple of output types if there are multiple outputs 56 | * - unknown if the function or its return type cannot be determined 57 | */ 58 | export type ContractFunctionReturnType< 59 | TAbi extends Abi, 60 | TFunctionName extends ContractFunctionName, 61 | TArgs extends Record = {}, 62 | > = 63 | MatchArgsToFunction extends infer TFunction 64 | ? TFunction extends AbiFunction 65 | ? AbiParametersToPrimitiveTypes extends infer TOutputs 66 | ? TOutputs extends readonly [] 67 | ? void 68 | : TOutputs extends readonly [infer TOutput] 69 | ? TOutput 70 | : TOutputs 71 | : any 72 | : any 73 | : any; 74 | -------------------------------------------------------------------------------- /src/coinbase/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Axios, AxiosResponse, InternalAxiosRequestConfig } from "axios"; 3 | import { APIError } from "./api_error"; 4 | import { InvalidUnsignedPayloadError } from "./errors"; 5 | 6 | /** 7 | * Prints Axios response to the console for debugging purposes. 8 | * 9 | * @param response - The Axios response object. 10 | * @param debugging - Flag to enable or disable logging. 11 | * @returns The Axios response object. 12 | */ 13 | export const logApiResponse = (response: AxiosResponse, debugging = false): AxiosResponse => { 14 | if (debugging) { 15 | let output = typeof response.data === "string" ? response.data : ""; 16 | 17 | if (typeof response.data === "object") { 18 | output = JSON.stringify(response.data, null, 4); 19 | } 20 | 21 | console.log(`API RESPONSE: 22 | Status: ${response.status} 23 | URL: ${response.config.url} 24 | Data: ${output}`); 25 | } 26 | return response; 27 | }; 28 | 29 | /** 30 | * Axios Request interceptor function type. 31 | * 32 | * @param value - The Axios request configuration. 33 | * @returns The modified Axios request configuration. 34 | */ 35 | type RequestFunctionType = ( 36 | value: InternalAxiosRequestConfig, 37 | ) => Promise | InternalAxiosRequestConfig; 38 | 39 | /** 40 | * Axios Response interceptor function type. 41 | * 42 | * @param value - The Axios response object. 43 | * @returns The modified Axios response object. 44 | */ 45 | type ResponseFunctionType = (value: AxiosResponse) => AxiosResponse; 46 | 47 | /** 48 | * Registers request and response interceptors to an Axios instance. 49 | * 50 | * @param axiosInstance - The Axios instance to register the interceptors. 51 | * @param requestFn - The request interceptor function. 52 | * @param responseFn - The response interceptor function. 53 | */ 54 | export const registerAxiosInterceptors = ( 55 | axiosInstance: Axios, 56 | requestFn: RequestFunctionType, 57 | responseFn: ResponseFunctionType, 58 | ) => { 59 | axiosInstance.interceptors.request.use(requestFn); 60 | axiosInstance.interceptors.response.use(responseFn, error => { 61 | return Promise.reject(APIError.fromError(error)); 62 | }); 63 | }; 64 | 65 | /** 66 | * Converts a Uint8Array to a hex string. 67 | * 68 | * @param key - The key to convert. 69 | * @returns The converted hex string. 70 | */ 71 | export const convertStringToHex = (key: Uint8Array): string => { 72 | return Buffer.from(key).toString("hex"); 73 | }; 74 | 75 | /** 76 | * Delays the execution of the function by the specified number of seconds. 77 | * 78 | * @param seconds - The number of seconds to delay the execution. 79 | * @returns A promise that resolves after the specified number of seconds. 80 | */ 81 | export async function delay(seconds: number): Promise { 82 | return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 83 | } 84 | 85 | /** 86 | * Parses an Unsigned Payload and returns the JSON object. 87 | * 88 | * @throws {InvalidUnsignedPayload} If the Unsigned Payload is invalid. 89 | * @param payload - The Unsigned Payload. 90 | * @returns The parsed JSON object. 91 | */ 92 | export function parseUnsignedPayload(payload: string): Record { 93 | const rawPayload = payload.match(/../g)?.map(byte => parseInt(byte, 16)); 94 | if (!rawPayload) { 95 | throw new InvalidUnsignedPayloadError("Unable to parse unsigned payload"); 96 | } 97 | 98 | let parsedPayload; 99 | try { 100 | const rawPayloadBytes = new Uint8Array(rawPayload); 101 | const decoder = new TextDecoder(); 102 | parsedPayload = JSON.parse(decoder.decode(rawPayloadBytes)); 103 | } catch (error) { 104 | throw new InvalidUnsignedPayloadError("Unable to decode unsigned payload JSON"); 105 | } 106 | 107 | return parsedPayload; 108 | } 109 | 110 | /** 111 | * Formats the input date to 'YYYY-MM-DD' 112 | * 113 | * @param date - The date to format. 114 | * 115 | * @returns a formated date of 'YYYY-MM-DD' 116 | */ 117 | export function formatDate(date: Date): string { 118 | const year = date.getFullYear(); 119 | const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based, so add 1 120 | const day = String(date.getDate()).padStart(2, "0"); 121 | return `${year}-${month}-${day}T00:00:00Z`; 122 | } 123 | 124 | /** 125 | * 126 | * Takes a date and subtracts a week from it. (7 days) 127 | * 128 | * @param date - The date to be formatted. 129 | * 130 | * @returns a formatted date that is one week ago. 131 | */ 132 | export function getWeekBackDate(date: Date): string { 133 | date.setDate(date.getDate() - 7); 134 | return formatDate(date); 135 | } 136 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./coinbase/address"; 2 | export * from "./coinbase/address/external_address"; 3 | export * from "./coinbase/address/wallet_address"; 4 | export * from "./coinbase/api_error"; 5 | export * from "./coinbase/asset"; 6 | export * from "./coinbase/authenticator"; 7 | export * from "./coinbase/balance"; 8 | export * from "./coinbase/balance_map"; 9 | export * from "./coinbase/coinbase"; 10 | export * from "./coinbase/constants"; 11 | export * from "./coinbase/contract_event"; 12 | export * from "./coinbase/contract_invocation"; 13 | export * from "./coinbase/errors"; 14 | export * from "./coinbase/faucet_transaction"; 15 | export * from "./coinbase/hash"; 16 | export * from "./coinbase/historical_balance"; 17 | export * from "./coinbase/payload_signature"; 18 | export * from "./coinbase/server_signer"; 19 | export * from "./coinbase/smart_contract"; 20 | export * from "./coinbase/staking_balance"; 21 | export * from "./coinbase/staking_operation"; 22 | export * from "./coinbase/staking_reward"; 23 | export * from "./coinbase/trade"; 24 | export * from "./coinbase/transaction"; 25 | export * from "./coinbase/transfer"; 26 | export * from "./coinbase/types"; 27 | export * from "./coinbase/validator"; 28 | export * from "./coinbase/wallet"; 29 | export * from "./coinbase/webhook"; 30 | export * from "./coinbase/read_contract"; 31 | export * from "./coinbase/crypto_amount"; 32 | export * from "./coinbase/fiat_amount"; 33 | export * from "./coinbase/fund_operation"; 34 | export * from "./coinbase/fund_quote"; 35 | export * from "./types/chain"; 36 | export * from "./wallets/types"; 37 | export * from "./wallets/createSmartWallet"; 38 | export * from "./wallets/toSmartWallet"; 39 | export * from "./actions/sendUserOperation"; 40 | export * from "./actions/waitForUserOperation"; 41 | -------------------------------------------------------------------------------- /src/tests/address_reputation_test.ts: -------------------------------------------------------------------------------- 1 | import { AddressReputation } from "../coinbase/address_reputation"; 2 | 3 | describe("AddressReputation", () => { 4 | let addressReputation: AddressReputation; 5 | 6 | beforeEach(() => { 7 | addressReputation = new AddressReputation({ 8 | score: -90, 9 | metadata: { 10 | unique_days_active: 1, 11 | total_transactions: 1, 12 | token_swaps_performed: 1, 13 | bridge_transactions_performed: 1, 14 | smart_contract_deployments: 1, 15 | longest_active_streak: 1, 16 | lend_borrow_stake_transactions: 1, 17 | ens_contract_interactions: 1, 18 | current_active_streak: 1, 19 | activity_period_days: 1, 20 | }, 21 | }); 22 | }); 23 | 24 | it("returns the score", () => { 25 | expect(addressReputation.score).toBe(-90); 26 | }); 27 | 28 | it("returns the metadata", () => { 29 | expect(addressReputation.metadata).toEqual({ 30 | unique_days_active: 1, 31 | total_transactions: 1, 32 | token_swaps_performed: 1, 33 | bridge_transactions_performed: 1, 34 | smart_contract_deployments: 1, 35 | longest_active_streak: 1, 36 | lend_borrow_stake_transactions: 1, 37 | ens_contract_interactions: 1, 38 | current_active_streak: 1, 39 | activity_period_days: 1, 40 | }); 41 | }); 42 | 43 | it("returns the string representation of the address reputation", () => { 44 | expect(addressReputation.toString()).toBe( 45 | "AddressReputation(score: -90, metadata: {unique_days_active: 1, total_transactions: 1, token_swaps_performed: 1, bridge_transactions_performed: 1, smart_contract_deployments: 1, longest_active_streak: 1, lend_borrow_stake_transactions: 1, ens_contract_interactions: 1, current_active_streak: 1, activity_period_days: 1})", 46 | ); 47 | }); 48 | 49 | it("should throw an error for an empty model", () => { 50 | expect(() => new AddressReputation(null!)).toThrow("Address reputation model cannot be empty"); 51 | }); 52 | 53 | describe("#risky", () => { 54 | it("returns the risky as true for score < 0", () => { 55 | expect(addressReputation.risky).toBe(true); 56 | }); 57 | 58 | it("should return risky as false for a score > 0", () => { 59 | addressReputation = new AddressReputation({ 60 | score: 90, 61 | metadata: { 62 | unique_days_active: 1, 63 | total_transactions: 1, 64 | token_swaps_performed: 1, 65 | bridge_transactions_performed: 1, 66 | smart_contract_deployments: 1, 67 | longest_active_streak: 1, 68 | lend_borrow_stake_transactions: 1, 69 | ens_contract_interactions: 1, 70 | current_active_streak: 1, 71 | activity_period_days: 1, 72 | }, 73 | }); 74 | expect(addressReputation.risky).toBe(false); 75 | }); 76 | 77 | it("should return risky as false for a score=0", () => { 78 | addressReputation = new AddressReputation({ 79 | score: 0, 80 | metadata: { 81 | unique_days_active: 1, 82 | total_transactions: 1, 83 | token_swaps_performed: 1, 84 | bridge_transactions_performed: 1, 85 | smart_contract_deployments: 1, 86 | longest_active_streak: 1, 87 | lend_borrow_stake_transactions: 1, 88 | ens_contract_interactions: 1, 89 | current_active_streak: 1, 90 | activity_period_days: 1, 91 | }, 92 | }); 93 | expect(addressReputation.risky).toBe(false); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/tests/api_error_test.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { 3 | APIError, 4 | UnimplementedError, 5 | UnauthorizedError, 6 | NotFoundError, 7 | InvalidWalletIDError, 8 | InvalidAddressIDError, 9 | InvalidWalletError, 10 | InvalidAddressError, 11 | InvalidAmountError, 12 | InvalidTransferIDError, 13 | InvalidPageError, 14 | InvalidLimitError, 15 | AlreadyExistsError, 16 | MalformedRequestError, 17 | UnsupportedAssetError, 18 | InvalidAssetIDError, 19 | InvalidDestinationError, 20 | InvalidNetworkIDError, 21 | ResourceExhaustedError, 22 | FaucetLimitReachedError, 23 | InvalidSignedPayloadError, 24 | InvalidTransferStatusError, 25 | NetworkFeatureUnsupportedError, 26 | } from "./../coinbase/api_error"; // Adjust the import path accordingly 27 | 28 | describe("APIError", () => { 29 | test("should create default APIError without response data", () => { 30 | const axiosError = new AxiosError("Network Error"); 31 | 32 | const apiError = new APIError(axiosError); 33 | 34 | expect(apiError.httpCode).toBeNull(); 35 | expect(apiError.apiCode).toBeNull(); 36 | expect(apiError.apiMessage).toBeNull(); 37 | expect(apiError.correlationId).toBeNull(); 38 | expect(apiError.toString()).toBe("APIError{}"); 39 | }); 40 | 41 | test("should create APIError with response data", () => { 42 | const axiosError = { 43 | response: { 44 | status: 400, 45 | data: { 46 | code: "invalid_wallet_id", 47 | message: "Invalid wallet ID", 48 | correlation_id: "123", 49 | }, 50 | }, 51 | } as AxiosError; 52 | 53 | const apiError = new APIError(axiosError); 54 | 55 | expect(apiError.httpCode).toBe(400); 56 | expect(apiError.apiCode).toBe("invalid_wallet_id"); 57 | expect(apiError.apiMessage).toBe("Invalid wallet ID"); 58 | expect(apiError.correlationId).toBe("123"); 59 | expect(apiError.toString()).toBe( 60 | "APIError{httpCode: 400, apiCode: invalid_wallet_id, apiMessage: Invalid wallet ID, correlationId: 123}", 61 | ); 62 | }); 63 | 64 | test.each([ 65 | ["unimplemented", UnimplementedError], 66 | ["unauthorized", UnauthorizedError], 67 | ["not_found", NotFoundError], 68 | ["invalid_wallet_id", InvalidWalletIDError], 69 | ["invalid_address_id", InvalidAddressIDError], 70 | ["invalid_wallet", InvalidWalletError], 71 | ["invalid_address", InvalidAddressError], 72 | ["invalid_amount", InvalidAmountError], 73 | ["invalid_transfer_id", InvalidTransferIDError], 74 | ["invalid_page_token", InvalidPageError], 75 | ["invalid_page_limit", InvalidLimitError], 76 | ["already_exists", AlreadyExistsError], 77 | ["malformed_request", MalformedRequestError], 78 | ["unsupported_asset", UnsupportedAssetError], 79 | ["invalid_asset_id", InvalidAssetIDError], 80 | ["invalid_destination", InvalidDestinationError], 81 | ["invalid_network_id", InvalidNetworkIDError], 82 | ["resource_exhausted", ResourceExhaustedError], 83 | ["faucet_limit_reached", FaucetLimitReachedError], 84 | ["invalid_signed_payload", InvalidSignedPayloadError], 85 | ["invalid_transfer_status", InvalidTransferStatusError], 86 | ["network_feature_unsupported", NetworkFeatureUnsupportedError], 87 | ])("should create %s error type", (code, ErrorType) => { 88 | const axiosError = { 89 | response: { 90 | status: 400, 91 | data: { 92 | code, 93 | message: "Error message", 94 | }, 95 | }, 96 | } as AxiosError; 97 | 98 | const apiError = APIError.fromError(axiosError); 99 | expect(apiError).toBeInstanceOf(ErrorType); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/tests/balance_map_test.ts: -------------------------------------------------------------------------------- 1 | import { BalanceMap } from "../coinbase/balance_map"; 2 | import { Balance as BalanceModel } from "../client"; 3 | import { Balance } from "../coinbase/balance"; 4 | import { Decimal } from "decimal.js"; 5 | import { Coinbase } from "../coinbase/coinbase"; 6 | 7 | describe("BalanceMap", () => { 8 | const ethAmount = new Decimal(1); 9 | const ethAtomicAmount = "1000000000000000000"; 10 | const usdcAmount = new Decimal(2); 11 | const usdcAtomicAmount = "2000000"; 12 | const wethAmount = new Decimal(3); 13 | const wethAtomicAmount = "3000000000000000000"; 14 | 15 | describe(".fromBalances", () => { 16 | const ethBalanceModel: BalanceModel = { 17 | asset: { 18 | asset_id: Coinbase.assets.Eth, 19 | network_id: Coinbase.networks.BaseSepolia, 20 | decimals: 18, 21 | contract_address: "0x", 22 | }, 23 | amount: ethAtomicAmount, 24 | }; 25 | 26 | const usdcBalanceModel: BalanceModel = { 27 | asset: { 28 | asset_id: "usdc", 29 | network_id: Coinbase.networks.BaseSepolia, 30 | decimals: 6, 31 | contract_address: "0x", 32 | }, 33 | amount: usdcAtomicAmount, 34 | }; 35 | 36 | const wethBalanceModel: BalanceModel = { 37 | asset: { 38 | asset_id: "weth", 39 | network_id: Coinbase.networks.BaseSepolia, 40 | decimals: 18, 41 | contract_address: "0x", 42 | }, 43 | amount: wethAtomicAmount, 44 | }; 45 | 46 | const balances = [ethBalanceModel, usdcBalanceModel, wethBalanceModel]; 47 | 48 | const balanceMap = BalanceMap.fromBalances(balances); 49 | 50 | it("returns a new BalanceMap object with the correct balances", () => { 51 | expect(balanceMap.get(Coinbase.assets.Eth)).toEqual(ethAmount); 52 | expect(balanceMap.get("usdc")).toEqual(usdcAmount); 53 | expect(balanceMap.get("weth")).toEqual(wethAmount); 54 | }); 55 | }); 56 | 57 | describe(".add", () => { 58 | const assetId = Coinbase.assets.Eth; 59 | const balance = Balance.fromModelAndAssetId( 60 | { 61 | amount: ethAtomicAmount, 62 | asset: { 63 | asset_id: assetId, 64 | network_id: Coinbase.networks.BaseSepolia, 65 | decimals: 18, 66 | contract_address: "0x", 67 | }, 68 | }, 69 | assetId, 70 | ); 71 | 72 | const balanceMap = new BalanceMap(); 73 | 74 | it("sets the amount", () => { 75 | balanceMap.add(balance); 76 | expect(balanceMap.get(assetId)).toEqual(ethAmount); 77 | }); 78 | 79 | it("throws an error if the balance parameter is not instance of Balance", () => { 80 | expect(() => balanceMap.add(null!)).toThrow(Error); 81 | }); 82 | }); 83 | 84 | describe(".toString", () => { 85 | const assetId = Coinbase.assets.Eth; 86 | const balance = Balance.fromModelAndAssetId( 87 | { 88 | amount: ethAtomicAmount, 89 | asset: { 90 | asset_id: assetId, 91 | network_id: Coinbase.networks.BaseSepolia, 92 | decimals: 18, 93 | contract_address: "0x", 94 | }, 95 | }, 96 | assetId, 97 | ); 98 | 99 | const balanceMap = new BalanceMap(); 100 | balanceMap.add(balance); 101 | 102 | it("returns a string representation of asset_id to floating-point number", () => { 103 | expect(balanceMap.toString()).toBe(`BalanceMap{"${assetId}":"${ethAmount}"}`); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/tests/balance_test.ts: -------------------------------------------------------------------------------- 1 | import { Balance } from "../coinbase/balance"; 2 | import { Balance as BalanceModel } from "../client"; 3 | import { Decimal } from "decimal.js"; 4 | import { Coinbase } from "../coinbase/coinbase"; 5 | 6 | describe("Balance", () => { 7 | describe(".fromModel", () => { 8 | const amount = new Decimal(1); 9 | const balanceModel: BalanceModel = { 10 | amount: "1000000000000000000", 11 | asset: { 12 | asset_id: Coinbase.assets.Eth, 13 | network_id: Coinbase.networks.BaseSepolia, 14 | decimals: 18, 15 | contract_address: "0x", 16 | }, 17 | }; 18 | 19 | const balance = Balance.fromModel(balanceModel); 20 | 21 | it("returns a new Balance object with the correct amount", () => { 22 | expect(balance.amount).toEqual(amount); 23 | }); 24 | 25 | it("returns a new Balance object with the correct asset_id", () => { 26 | expect(balance.assetId).toBe(Coinbase.assets.Eth); 27 | }); 28 | }); 29 | 30 | describe(".fromModelAndAssetId", () => { 31 | const amount = new Decimal(1); 32 | const balanceModel: BalanceModel = { 33 | asset: { 34 | asset_id: Coinbase.assets.Eth, 35 | network_id: Coinbase.networks.BaseSepolia, 36 | decimals: 18, 37 | contract_address: "0x", 38 | }, 39 | amount: "1000000000000000000", 40 | }; 41 | 42 | const balance = Balance.fromModelAndAssetId(balanceModel, Coinbase.assets.Eth); 43 | 44 | it("returns a new Balance object with the correct amount", () => { 45 | expect(balance.amount).toEqual(amount); 46 | }); 47 | 48 | it("returns a new Balance object with the correct asset_id", () => { 49 | expect(balance.assetId).toBe(Coinbase.assets.Eth); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/tests/config/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": 0, 3 | "apiSecret": "" 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/config/not_parseable.json: -------------------------------------------------------------------------------- 1 | not parseable content -------------------------------------------------------------------------------- /src/tests/config/test_api_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "organizations/b6d44812-a3ea-4b0c-92c3-029a37010a2e/apiKeys/3e752529-b9be-4bd4-802e-bfe769c0ab56", 3 | "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" 4 | } -------------------------------------------------------------------------------- /src/tests/config/test_api_key_with_only_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3e752529-b9be-4bd4-802e-bfe769c0ab56", 3 | "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBPl8LBKrDw2Is+bxQEXa2eHhDmvIgArOhSAdmYpYQrCoAoGCCqGSM49\nAwEHoUQDQgAEQSoVSr8ImpS18thpGe3KuL9efy+L+AFdFFfCVwGgCsKvTYVDKaGo\nVmN5Bl6EJkeIQjyarEtWbmY6komwEOdnHA==\n-----END EC PRIVATE KEY-----\n" 4 | } -------------------------------------------------------------------------------- /src/tests/config/test_ed25519_api_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "organizations/b6d44812-a3ea-4b0c-92c3-029a37010a2e/apiKeys/3e752529-b9be-4bd4-802e-bfe769c0ab56", 3 | "privateKey": "hnW9bgWmEjiioG+H8RdcerLEvh7HgWiixtzBKX4T3YkHN7G3Vt5zUPqfPVnTQYeRfIXN1scvhXWo8guFoqElXg==" 4 | } -------------------------------------------------------------------------------- /src/tests/config/test_ed25519_api_key_with_only_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3e752529-b9be-4bd4-802e-bfe769c0ab56", 3 | "privateKey": "hnW9bgWmEjiioG+H8RdcerLEvh7HgWiixtzBKX4T3YkHN7G3Vt5zUPqfPVnTQYeRfIXN1scvhXWo8guFoqElXg==" 4 | } -------------------------------------------------------------------------------- /src/tests/contract_event_test.ts: -------------------------------------------------------------------------------- 1 | import { ContractEvent } from "../coinbase/contract_event"; 2 | 3 | describe("ContractEvent", () => { 4 | const eventData = { 5 | network_id: "ethereum-mainnet", 6 | protocol_name: "uniswap", 7 | contract_name: "Pool", 8 | event_name: "Transfer", 9 | sig: "Transfer(address,address,uint256)", 10 | four_bytes: "0xddf252ad", 11 | contract_address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 12 | block_time: "2023-04-01T12:00:00Z", 13 | block_height: 201782330, 14 | tx_hash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 15 | tx_index: 109, 16 | event_index: 362, 17 | data: '{"from":"0x1234...","to":"0x5678...","value":"1000000000000000000"}', 18 | }; 19 | 20 | describe(".networkId", () => { 21 | it("should return the correct network ID", () => { 22 | const event = new ContractEvent(eventData); 23 | expect(event.networkId()).toEqual("ethereum-mainnet"); 24 | }); 25 | }); 26 | 27 | describe(".protocolName", () => { 28 | it("should return the correct protocol name", () => { 29 | const event = new ContractEvent(eventData); 30 | expect(event.protocolName()).toEqual("uniswap"); 31 | }); 32 | }); 33 | 34 | describe(".contractName", () => { 35 | it("should return the correct contract name", () => { 36 | const event = new ContractEvent(eventData); 37 | expect(event.contractName()).toEqual("Pool"); 38 | }); 39 | }); 40 | 41 | describe(".eventName", () => { 42 | it("should return the correct event name", () => { 43 | const event = new ContractEvent(eventData); 44 | expect(event.eventName()).toEqual("Transfer"); 45 | }); 46 | }); 47 | 48 | describe(".sig", () => { 49 | it("should return the correct signature", () => { 50 | const event = new ContractEvent(eventData); 51 | expect(event.sig()).toEqual("Transfer(address,address,uint256)"); 52 | }); 53 | }); 54 | 55 | describe(".fourBytes", () => { 56 | it("should return the correct four bytes", () => { 57 | const event = new ContractEvent(eventData); 58 | expect(event.fourBytes()).toEqual("0xddf252ad"); 59 | }); 60 | }); 61 | 62 | describe(".contractAddress", () => { 63 | it("should return the correct contract address", () => { 64 | const event = new ContractEvent(eventData); 65 | expect(event.contractAddress()).toEqual("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); 66 | }); 67 | }); 68 | 69 | describe(".blockTime", () => { 70 | it("should return the correct block time", () => { 71 | const event = new ContractEvent(eventData); 72 | expect(event.blockTime()).toEqual(new Date("2023-04-01T12:00:00Z")); 73 | }); 74 | }); 75 | 76 | describe(".blockHeight", () => { 77 | it("should return the correct block height", () => { 78 | const event = new ContractEvent(eventData); 79 | expect(event.blockHeight()).toEqual(201782330); 80 | }); 81 | }); 82 | 83 | describe(".txHash", () => { 84 | it("should return the correct transaction hash", () => { 85 | const event = new ContractEvent(eventData); 86 | expect(event.txHash()).toEqual( 87 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 88 | ); 89 | }); 90 | }); 91 | 92 | describe(".txIndex", () => { 93 | it("should return the correct transaction index", () => { 94 | const event = new ContractEvent(eventData); 95 | expect(event.txIndex()).toEqual(109); 96 | }); 97 | }); 98 | 99 | describe(".eventIndex", () => { 100 | it("should return the correct event index", () => { 101 | const event = new ContractEvent(eventData); 102 | expect(event.eventIndex()).toEqual(362); 103 | }); 104 | }); 105 | 106 | describe(".data", () => { 107 | it("should return the correct event data", () => { 108 | const event = new ContractEvent(eventData); 109 | expect(event.data()).toEqual( 110 | '{"from":"0x1234...","to":"0x5678...","value":"1000000000000000000"}', 111 | ); 112 | }); 113 | }); 114 | 115 | describe(".toString", () => { 116 | it("should return the string representation of a contract event", () => { 117 | const event = new ContractEvent(eventData); 118 | const eventStr = event.toString(); 119 | expect(eventStr).toEqual( 120 | "ContractEvent { networkId: 'ethereum-mainnet' protocolName: 'uniswap' contractName: 'Pool' eventName: 'Transfer' contractAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' blockHeight: 201782330 txHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' }", 121 | ); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/tests/crypto_amount_test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "@jest/globals"; 2 | import Decimal from "decimal.js"; 3 | import { CryptoAmount } from "../coinbase/crypto_amount"; 4 | import { Asset } from "../coinbase/asset"; 5 | import { CryptoAmount as CryptoAmountModel } from "../client/api"; 6 | import { Coinbase } from "../coinbase/coinbase"; 7 | import { 8 | contractInvocationApiMock, 9 | getAssetMock, 10 | VALID_ETH_CRYPTO_AMOUNT_MODEL, 11 | VALID_USDC_CRYPTO_AMOUNT_MODEL, 12 | } from "./utils"; 13 | import { ContractInvocation } from "../coinbase/contract_invocation"; 14 | 15 | describe("CryptoAmount", () => { 16 | let cryptoAmountModel: CryptoAmountModel; 17 | let cryptoAmount: CryptoAmount; 18 | 19 | beforeEach(() => { 20 | cryptoAmountModel = VALID_USDC_CRYPTO_AMOUNT_MODEL; 21 | cryptoAmount = CryptoAmount.fromModel(cryptoAmountModel); 22 | }); 23 | 24 | afterEach(() => { 25 | jest.restoreAllMocks(); 26 | }); 27 | 28 | describe(".fromModel", () => { 29 | it("should correctly create CryptoAmount from model", () => { 30 | expect(cryptoAmount).toBeInstanceOf(CryptoAmount); 31 | expect(cryptoAmount.getAmount().equals(new Decimal(1).div(new Decimal(10).pow(6)))); 32 | expect(cryptoAmount.getAsset().assetId).toEqual(Coinbase.assets.Usdc); 33 | expect(cryptoAmount.getAsset().networkId).toEqual("base-sepolia"); 34 | expect(cryptoAmount.getAsset().decimals).toEqual(6); 35 | }); 36 | }); 37 | 38 | describe(".fromModelAndAssetId", () => { 39 | it("should correctly create CryptoAmount from model with gwei denomination", () => { 40 | const cryptoAmount = CryptoAmount.fromModelAndAssetId( 41 | VALID_ETH_CRYPTO_AMOUNT_MODEL, 42 | Coinbase.assets.Gwei, 43 | ); 44 | expect(cryptoAmount.getAmount().equals(new Decimal(1).div(new Decimal(10).pow(9)))); 45 | expect(cryptoAmount.getAsset().assetId).toEqual(Coinbase.assets.Gwei); 46 | expect(cryptoAmount.getAsset().networkId).toEqual("base-sepolia"); 47 | expect(cryptoAmount.getAsset().decimals).toEqual(9); 48 | }); 49 | 50 | it("should correctly create CryptoAmount from model with wei denomination", () => { 51 | const cryptoAmount = CryptoAmount.fromModelAndAssetId( 52 | VALID_ETH_CRYPTO_AMOUNT_MODEL, 53 | Coinbase.assets.Wei, 54 | ); 55 | expect(cryptoAmount.getAmount().equals(new Decimal(1))); 56 | expect(cryptoAmount.getAsset().assetId).toEqual(Coinbase.assets.Wei); 57 | expect(cryptoAmount.getAsset().networkId).toEqual("base-sepolia"); 58 | expect(cryptoAmount.getAsset().decimals).toEqual(0); 59 | }); 60 | }); 61 | 62 | describe("#getAmount", () => { 63 | it("should return the correct amount", () => { 64 | expect(cryptoAmount.getAmount().equals(new Decimal(1).div(new Decimal(10).pow(6)))); 65 | }); 66 | }); 67 | 68 | describe("#getAsset", () => { 69 | it("should return the correct asset", () => { 70 | expect(cryptoAmount.getAsset().assetId).toEqual(Coinbase.assets.Usdc); 71 | expect(cryptoAmount.getAsset().networkId).toEqual("base-sepolia"); 72 | expect(cryptoAmount.getAsset().decimals).toEqual(6); 73 | }); 74 | }); 75 | 76 | describe("#getAssetId", () => { 77 | it("should return the correct asset ID", () => { 78 | expect(cryptoAmount.getAssetId()).toEqual(Coinbase.assets.Usdc); 79 | }); 80 | }); 81 | 82 | describe("#toAtomicAmount", () => { 83 | it("should correctly convert to atomic amount", () => { 84 | expect(cryptoAmount.toAtomicAmount().toString()).toEqual("1"); 85 | }); 86 | }); 87 | 88 | describe("#toString", () => { 89 | it("should have correct string representation", () => { 90 | expect(cryptoAmount.toString()).toEqual("CryptoAmount{amount: '0.000001', assetId: 'usdc'}"); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/tests/error_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentError, 3 | InvalidAPIKeyFormatError, 4 | InvalidConfigurationError, 5 | InvalidUnsignedPayloadError, 6 | AlreadySignedError, 7 | } from "../coinbase/errors"; 8 | 9 | describe("Error Classes", () => { 10 | it("InvalidAPIKeyFormatError should have the correct message and name", () => { 11 | const error = new InvalidAPIKeyFormatError(); 12 | expect(error.message).toBe(InvalidAPIKeyFormatError.DEFAULT_MESSAGE); 13 | expect(error.name).toBe("InvalidAPIKeyFormatError"); 14 | }); 15 | 16 | it("InvalidAPIKeyFormatError should accept a custom message", () => { 17 | const customMessage = "Custom invalid API key format message"; 18 | const error = new InvalidAPIKeyFormatError(customMessage); 19 | expect(error.message).toBe(customMessage); 20 | }); 21 | 22 | it("ArgumentError should have the correct message and name", () => { 23 | const error = new ArgumentError(); 24 | expect(error.message).toBe(ArgumentError.DEFAULT_MESSAGE); 25 | expect(error.name).toBe("ArgumentError"); 26 | }); 27 | 28 | it("ArgumentError should accept a custom message", () => { 29 | const customMessage = "Custom argument error message"; 30 | const error = new ArgumentError(customMessage); 31 | expect(error.message).toBe(customMessage); 32 | }); 33 | 34 | it("InvalidConfigurationError should have the correct message and name", () => { 35 | const error = new InvalidConfigurationError(); 36 | expect(error.message).toBe(InvalidConfigurationError.DEFAULT_MESSAGE); 37 | expect(error.name).toBe("InvalidConfigurationError"); 38 | }); 39 | 40 | it("InvalidConfigurationError should accept a custom message", () => { 41 | const customMessage = "Custom invalid configuration message"; 42 | const error = new InvalidConfigurationError(customMessage); 43 | expect(error.message).toBe(customMessage); 44 | }); 45 | 46 | it("InvalidUnsignedPayloadError should have the correct message and name", () => { 47 | const error = new InvalidUnsignedPayloadError(); 48 | expect(error.message).toBe(InvalidUnsignedPayloadError.DEFAULT_MESSAGE); 49 | expect(error.name).toBe("InvalidUnsignedPayloadError"); 50 | }); 51 | 52 | it("InvalidUnsignedPayloadError should accept a custom message", () => { 53 | const customMessage = "Custom invalid unsigned payload message"; 54 | const error = new InvalidUnsignedPayloadError(customMessage); 55 | expect(error.message).toBe(customMessage); 56 | }); 57 | 58 | it("AlreadySignedError should have the correct message and name", () => { 59 | const error = new AlreadySignedError(); 60 | expect(error.message).toBe(AlreadySignedError.DEFAULT_MESSAGE); 61 | expect(error.name).toBe("AlreadySignedError"); 62 | }); 63 | 64 | it("AlreadySignedError should accept a custom message", () => { 65 | const customMessage = "Custom already signed error message"; 66 | const error = new AlreadySignedError(customMessage); 67 | expect(error.message).toBe(customMessage); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/tests/fiat_amount_test.ts: -------------------------------------------------------------------------------- 1 | import { FiatAmount } from "../coinbase/fiat_amount"; 2 | import { FiatAmount as FiatAmountModel } from "../client/api"; 3 | 4 | describe("FiatAmount", () => { 5 | describe(".fromModel", () => { 6 | it("should convert a FiatAmount model to a FiatAmount", () => { 7 | const model: FiatAmountModel = { 8 | amount: "100.50", 9 | currency: "USD", 10 | }; 11 | 12 | const fiatAmount = FiatAmount.fromModel(model); 13 | 14 | expect(fiatAmount.getAmount()).toBe("100.50"); 15 | expect(fiatAmount.getCurrency()).toBe("USD"); 16 | }); 17 | }); 18 | 19 | describe("#getAmount", () => { 20 | it("should return the correct amount", () => { 21 | const fiatAmount = new FiatAmount("50.25", "USD"); 22 | expect(fiatAmount.getAmount()).toBe("50.25"); 23 | }); 24 | }); 25 | 26 | describe("#getCurrency", () => { 27 | it("should return the correct currency", () => { 28 | const fiatAmount = new FiatAmount("50.25", "USD"); 29 | expect(fiatAmount.getCurrency()).toBe("USD"); 30 | }); 31 | }); 32 | 33 | describe("#toString", () => { 34 | it("should return the correct string representation", () => { 35 | const fiatAmount = new FiatAmount("75.00", "USD"); 36 | const expectedStr = "FiatAmount(amount: '75.00', currency: 'USD')"; 37 | 38 | expect(fiatAmount.toString()).toBe(expectedStr); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/hash_test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { hashMessage, hashTypedDataMessage } from "../coinbase/hash"; 3 | 4 | describe("hashMessage", () => { 5 | const mockHashMessage = jest.spyOn(ethers, "hashMessage"); 6 | 7 | beforeEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it("should hash a string message correctly using EIP-191", () => { 12 | const message = "Hello, Ethereum!"; 13 | const expectedHash = "0xExpectedHash"; 14 | 15 | mockHashMessage.mockReturnValue(expectedHash); 16 | 17 | const result = hashMessage(message); 18 | expect(result).toBe(expectedHash); 19 | expect(mockHashMessage).toHaveBeenCalledWith(message); 20 | expect(mockHashMessage).toHaveBeenCalledTimes(1); 21 | }); 22 | 23 | it("should throw an error if ethers throws an error", () => { 24 | const invalidMessage = 12345; 25 | const expectedError = new Error("invalid message"); 26 | 27 | mockHashMessage.mockImplementation(() => { 28 | throw expectedError; 29 | }); 30 | 31 | expect(() => hashMessage(invalidMessage as any)).toThrow(expectedError); 32 | expect(mockHashMessage).toHaveBeenCalledWith(invalidMessage); 33 | expect(mockHashMessage).toHaveBeenCalledTimes(1); 34 | }); 35 | }); 36 | 37 | describe("hashTypedDataMessage", () => { 38 | const mockTypedDataEncoderHash = jest.spyOn(ethers.TypedDataEncoder, "hash"); 39 | 40 | beforeEach(() => { 41 | jest.clearAllMocks(); 42 | }); 43 | 44 | it("should hash typed data message correctly using EIP-712", () => { 45 | const domain = { 46 | name: "Ether Mail", 47 | version: "1", 48 | chainId: 1, 49 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 50 | }; 51 | 52 | const types = { 53 | Person: [ 54 | { name: "name", type: "string" }, 55 | { name: "wallet", type: "address" }, 56 | ], 57 | }; 58 | 59 | const value = { 60 | name: "Alice", 61 | wallet: "0x123456789abcdef123456789abcdef123456789a", 62 | }; 63 | 64 | const expectedHash = "0xExpectedHash"; 65 | 66 | mockTypedDataEncoderHash.mockReturnValue(expectedHash); 67 | 68 | const result = hashTypedDataMessage(domain, types, value); 69 | expect(result).toBe(expectedHash); 70 | expect(mockTypedDataEncoderHash).toHaveBeenCalledWith(domain, types, value); 71 | expect(mockTypedDataEncoderHash).toHaveBeenCalledTimes(1); 72 | }); 73 | 74 | it("should throw an error if ethers throws an error", () => { 75 | const domain = { 76 | name: "Invalid", 77 | version: "1", 78 | chainId: 1, 79 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 80 | }; 81 | 82 | const types = { 83 | Person: [ 84 | { name: "name", type: "string" }, 85 | { name: "wallet", type: "address" }, 86 | ], 87 | }; 88 | 89 | const value = { 90 | name: "InvalidName", 91 | wallet: "invalidWallet", 92 | }; 93 | 94 | const expectedError = new Error("invalid typed data message"); 95 | 96 | mockTypedDataEncoderHash.mockImplementation(() => { 97 | throw expectedError; 98 | }); 99 | 100 | expect(() => { 101 | hashTypedDataMessage(domain, types, value); 102 | }).toThrow(expectedError); 103 | expect(mockTypedDataEncoderHash).toHaveBeenCalledWith(domain, types, value); 104 | expect(mockTypedDataEncoderHash).toHaveBeenCalledTimes(1); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/tests/historical_balance_test.ts: -------------------------------------------------------------------------------- 1 | import { HistoricalBalance } from "../coinbase/historical_balance"; 2 | import { HistoricalBalance as HistoricalBalanceModel } from "../client"; 3 | import { Decimal } from "decimal.js"; 4 | import { Coinbase } from "../coinbase/coinbase"; 5 | 6 | describe("HistoricalBalance", () => { 7 | describe("#fromModel", () => { 8 | const amount = new Decimal(1); 9 | const historyModel: HistoricalBalanceModel = { 10 | amount: "1000000000000000000", 11 | block_hash: "0x0dadd465fb063ceb78babbb30abbc6bfc0730d0c57a53e8f6dc778dafcea568f", 12 | block_height: "11349306", 13 | asset: { 14 | asset_id: Coinbase.assets.Eth, 15 | network_id: Coinbase.networks.BaseSepolia, 16 | decimals: 18, 17 | contract_address: "0x", 18 | }, 19 | }; 20 | 21 | const historicalBalance = HistoricalBalance.fromModel(historyModel); 22 | 23 | it("returns a new HistoricalBalance object with the correct amount", () => { 24 | expect(historicalBalance.amount).toEqual(amount); 25 | }); 26 | 27 | it("returns a new HistoricalBalance object with the correct asset_id", () => { 28 | expect(historicalBalance.asset.assetId).toBe(Coinbase.assets.Eth); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/tests/index_test.ts: -------------------------------------------------------------------------------- 1 | // test/index.test.ts 2 | import * as index from "../index"; 3 | 4 | describe("Index file exports", () => { 5 | it("should export all modules correctly", () => { 6 | expect(index).toBeDefined(); 7 | expect(index).toHaveProperty("Address"); 8 | expect(index).toHaveProperty("APIError"); 9 | expect(index).toHaveProperty("Asset"); 10 | expect(index).toHaveProperty("Balance"); 11 | expect(index).toHaveProperty("BalanceMap"); 12 | expect(index).toHaveProperty("Coinbase"); 13 | expect(index).toHaveProperty("ContractEvent"); 14 | expect(index).toHaveProperty("ContractInvocation"); 15 | expect(index).toHaveProperty("ExternalAddress"); 16 | expect(index).toHaveProperty("FaucetTransaction"); 17 | expect(index).toHaveProperty("GWEI_DECIMALS"); 18 | expect(index).toHaveProperty("HistoricalBalance"); 19 | expect(index).toHaveProperty("InvalidAPIKeyFormatError"); 20 | expect(index).toHaveProperty("PayloadSignature"); 21 | expect(index).toHaveProperty("ServerSigner"); 22 | expect(index).toHaveProperty("SmartContract"); 23 | expect(index).toHaveProperty("SponsoredSendStatus"); 24 | expect(index).toHaveProperty("StakeOptionsMode"); 25 | expect(index).toHaveProperty("StakingBalance"); 26 | expect(index).toHaveProperty("StakingOperation"); 27 | expect(index).toHaveProperty("StakingReward"); 28 | expect(index).toHaveProperty("Trade"); 29 | expect(index).toHaveProperty("Transaction"); 30 | expect(index).toHaveProperty("TransactionStatus"); 31 | expect(index).toHaveProperty("Transfer"); 32 | expect(index).toHaveProperty("TransferStatus"); 33 | expect(index).toHaveProperty("Validator"); 34 | expect(index).toHaveProperty("Wallet"); 35 | expect(index).toHaveProperty("WalletAddress"); 36 | expect(index).toHaveProperty("Webhook"); 37 | expect(index).toHaveProperty("CryptoAmount"); 38 | expect(index).toHaveProperty("FiatAmount"); 39 | expect(index).toHaveProperty("FundOperation"); 40 | expect(index).toHaveProperty("FundQuote"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/tests/server_signer_test.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import { Coinbase } from "../coinbase/coinbase"; 3 | import { APIError } from "../coinbase/api_error"; 4 | import { ServerSigner as ServerSignerModel, ServerSignerList } from "../client"; 5 | import { serverSignersApiMock, mockReturnValue, mockReturnRejectedValue } from "./utils"; 6 | import { ServerSigner } from "../coinbase/server_signer"; 7 | 8 | describe("ServerSigner", () => { 9 | let serverSigner: ServerSigner; 10 | const serverSignerId: string = crypto.randomUUID(); 11 | const wallets: string[] = Array.from({ length: 3 }, () => crypto.randomUUID()); 12 | const model: ServerSignerModel = { 13 | server_signer_id: serverSignerId, 14 | wallets: wallets, 15 | is_mpc: true, 16 | }; 17 | const serverSignerList: ServerSignerList = { 18 | data: [model], 19 | total_count: 1, 20 | has_more: false, 21 | next_page: "", 22 | }; 23 | const emptyServerSignerList: ServerSignerList = { 24 | data: [], 25 | total_count: 0, 26 | has_more: false, 27 | next_page: "", 28 | }; 29 | 30 | beforeAll(async () => { 31 | Coinbase.apiClients.serverSigner = serverSignersApiMock; 32 | Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnValue(serverSignerList); 33 | serverSigner = await ServerSigner.getDefault(); 34 | }); 35 | 36 | afterEach(() => { 37 | jest.restoreAllMocks(); 38 | }); 39 | 40 | describe(".getDefault", () => { 41 | describe("when a default Server-Signer exists", () => { 42 | beforeEach(() => { 43 | Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnValue(serverSignerList); 44 | }); 45 | 46 | it("should return the default Server-Signer", async () => { 47 | const defaultServerSigner = await ServerSigner.getDefault(); 48 | expect(defaultServerSigner).toBeInstanceOf(ServerSigner); 49 | expect(defaultServerSigner.getId()).toBe(serverSignerId); 50 | expect(defaultServerSigner.getWallets()).toBe(wallets); 51 | expect(Coinbase.apiClients.serverSigner!.listServerSigners).toHaveBeenCalledTimes(1); 52 | }); 53 | }); 54 | 55 | it("should throw an APIError when the request is unsuccessful", async () => { 56 | Coinbase.apiClients.serverSigner!.listServerSigners = mockReturnRejectedValue( 57 | new APIError("Failed to list Server-Signers"), 58 | ); 59 | await expect(ServerSigner.getDefault()).rejects.toThrow(APIError); 60 | expect(Coinbase.apiClients.serverSigner!.listServerSigners).toHaveBeenCalledTimes(1); 61 | }); 62 | 63 | describe("when a default Server-Signer does not exist", () => { 64 | beforeEach(() => { 65 | Coinbase.apiClients.serverSigner!.listServerSigners = 66 | mockReturnValue(emptyServerSignerList); 67 | }); 68 | 69 | it("should return an error", async () => { 70 | await expect(ServerSigner.getDefault()).rejects.toThrow( 71 | new Error("No Server-Signer is associated with the project"), 72 | ); 73 | }); 74 | }); 75 | }); 76 | 77 | describe("#getId", () => { 78 | it("should return the Server-Signer ID", async () => { 79 | expect(serverSigner.getId()).toBe(serverSignerId); 80 | }); 81 | }); 82 | 83 | describe("#getWallets", () => { 84 | it("should return the list of Wallet IDs", async () => { 85 | expect(serverSigner.getWallets()).toBe(wallets); 86 | }); 87 | }); 88 | 89 | describe("#toString", () => { 90 | it("should return the correct string representation", async () => { 91 | expect(serverSigner.toString()).toBe( 92 | `ServerSigner{id: '${serverSignerId}', wallets: '${wallets}'}`, 93 | ); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/tests/utils_test.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from "axios"; 2 | import { InvalidUnsignedPayloadError } from "../coinbase/errors"; 3 | import { logApiResponse, parseUnsignedPayload } from "./../coinbase/utils"; // Adjust the path as necessary 4 | 5 | describe("parseUnsignedPayload", () => { 6 | it("should parse and return a valid payload", () => { 7 | const payload = "7b226b6579223a2276616c7565227d"; // {"key":"value"} in hex 8 | const expectedOutput = { key: "value" }; 9 | expect(parseUnsignedPayload(payload)).toEqual(expectedOutput); 10 | }); 11 | 12 | it("should throw InvalidUnsignedPayload error if payload cannot be parsed", () => { 13 | const payload = "invalidhexstring"; 14 | expect(() => parseUnsignedPayload(payload)).toThrow(InvalidUnsignedPayloadError); 15 | }); 16 | 17 | it("should throw InvalidUnsignedPayload error if payload cannot be decoded to JSON", () => { 18 | const payload = "000102"; // Invalid JSON 19 | expect(() => parseUnsignedPayload(payload)).toThrow(InvalidUnsignedPayloadError); 20 | }); 21 | 22 | it("should throw InvalidUnsignedPayload error if payload is an empty string", () => { 23 | const payload = ""; 24 | expect(() => parseUnsignedPayload(payload)).toThrow(InvalidUnsignedPayloadError); 25 | }); 26 | 27 | it("should throw InvalidUnsignedPayload error if payload contains non-hex characters", () => { 28 | const payload = "7b226b6579223a2276616c75657g7d"; // Invalid hex due to 'g' 29 | expect(() => parseUnsignedPayload(payload)).toThrow(InvalidUnsignedPayloadError); 30 | }); 31 | }); 32 | 33 | describe("logApiResponse", () => { 34 | let consoleSpy: jest.SpyInstance; 35 | 36 | beforeEach(() => { 37 | consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {}); 38 | }); 39 | 40 | afterEach(() => { 41 | consoleSpy.mockRestore(); 42 | }); 43 | 44 | it("should log response data as string when debugging is true and response data is a string", () => { 45 | const response = { 46 | data: "Response data", 47 | status: 200, 48 | statusText: "OK", 49 | headers: {}, 50 | config: { url: "http://example.com" }, 51 | } as AxiosResponse; 52 | logApiResponse(response, true); 53 | expect(consoleSpy).toHaveBeenCalledWith(`API RESPONSE: 54 | Status: ${response.status} 55 | URL: ${response.config.url} 56 | Data: ${response.data}`); 57 | }); 58 | 59 | it("should log response data as JSON string when debugging is true and response data is an object", () => { 60 | const response = { 61 | data: { key: "value" }, 62 | status: 200, 63 | statusText: "OK", 64 | headers: {}, 65 | config: { url: "http://example.com" }, 66 | } as AxiosResponse; 67 | const expectedOutput = JSON.stringify(response.data, null, 4); 68 | logApiResponse(response, true); 69 | expect(consoleSpy).toHaveBeenCalledWith(`API RESPONSE: 70 | Status: ${response.status} 71 | URL: ${response.config.url} 72 | Data: ${expectedOutput}`); 73 | }); 74 | 75 | it("should not log anything when debugging is false", () => { 76 | const response = { 77 | data: { key: "value" }, 78 | status: 200, 79 | statusText: "OK", 80 | headers: {}, 81 | config: { url: "http://example.com" }, 82 | } as AxiosResponse; 83 | logApiResponse(response, false); 84 | expect(consoleSpy).not.toHaveBeenCalled(); 85 | }); 86 | 87 | it("should return the response object", () => { 88 | const response = { 89 | data: { key: "value" }, 90 | status: 200, 91 | statusText: "OK", 92 | headers: {}, 93 | config: { url: "http://example.com" }, 94 | } as AxiosResponse; 95 | const result = logApiResponse(response, false); 96 | expect(result).toBe(response); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/tests/validator_test.ts: -------------------------------------------------------------------------------- 1 | import { Validator } from "../coinbase/validator"; 2 | import { ValidatorStatus } from "../coinbase/types"; 3 | import { Validator as ValidatorModel } from "../client"; 4 | import { Coinbase } from "../coinbase/coinbase"; 5 | import { ValidatorStatus as APIValidatorStatus } from "../client/api"; 6 | 7 | describe("Validator", () => { 8 | let validator: Validator; 9 | 10 | beforeEach(() => { 11 | const mockModel: ValidatorModel = { 12 | validator_id: "123", 13 | status: APIValidatorStatus.Active, 14 | network_id: Coinbase.networks.EthereumHoodi, 15 | asset_id: Coinbase.assets.Eth, 16 | details: { 17 | effective_balance: { 18 | amount: "100", 19 | asset: { network_id: Coinbase.networks.EthereumHoodi, asset_id: Coinbase.assets.Eth }, 20 | }, 21 | balance: { 22 | amount: "200", 23 | asset: { network_id: Coinbase.networks.EthereumHoodi, asset_id: Coinbase.assets.Eth }, 24 | }, 25 | exitEpoch: "epoch-1", 26 | activationEpoch: "epoch-0", 27 | index: "0", 28 | public_key: "public-key-123", 29 | slashed: false, 30 | withdrawableEpoch: "epoch-2", 31 | withdrawal_address: "withdrawal-address-123", 32 | withdrawal_credentials: "withdrawal-credentials-123", 33 | fee_recipient_address: "fee-recipient-address-123", 34 | }, 35 | }; 36 | 37 | validator = new Validator(mockModel); 38 | }); 39 | 40 | test("getValidatorId should return the correct validator ID", () => { 41 | expect(validator.getValidatorId()).toBe("123"); 42 | }); 43 | 44 | test("getStatus should return the correct status", () => { 45 | expect(validator.getStatus()).toBe(ValidatorStatus.ACTIVE); 46 | }); 47 | 48 | test("getNetworkId should return the correct network ID", () => { 49 | expect(validator.getNetworkId()).toBe(Coinbase.networks.EthereumHoodi); 50 | }); 51 | 52 | test("getAssetId should return the correct asset ID", () => { 53 | expect(validator.getAssetId()).toBe(Coinbase.assets.Eth); 54 | }); 55 | 56 | test("getActivationEpoch should return the correct activation epoch", () => { 57 | expect(validator.getActivationEpoch()).toBe("epoch-0"); 58 | }); 59 | 60 | test("getExitEpoch should return the correct exit epoch", () => { 61 | expect(validator.getExitEpoch()).toBe("epoch-1"); 62 | }); 63 | 64 | test("getIndex should return the correct index", () => { 65 | expect(validator.getIndex()).toBe("0"); 66 | }); 67 | 68 | test("getPublicKey should return the correct public key", () => { 69 | expect(validator.getPublicKey()).toBe("public-key-123"); 70 | }); 71 | 72 | test("isSlashed should return the correct slashed status", () => { 73 | expect(validator.isSlashed()).toBe(false); 74 | }); 75 | 76 | test("getWithdrawableEpoch should return the correct withdrawable epoch", () => { 77 | expect(validator.getWithdrawableEpoch()).toBe("epoch-2"); 78 | }); 79 | 80 | test("getWithdrawalAddress should return the correct withdrawal address", () => { 81 | expect(validator.getWithdrawalAddress()).toBe("withdrawal-address-123"); 82 | }); 83 | 84 | test("getWithdrawalCredentials should return the correct withdrawal credentials", () => { 85 | expect(validator.getWithdrawalCredentials()).toBe("withdrawal-credentials-123"); 86 | }); 87 | 88 | test("getFeeRecipientAddress should return the correct fee recipient address", () => { 89 | expect(validator.getFeeRecipientAddress()).toBe("fee-recipient-address-123"); 90 | }); 91 | 92 | test("getForwardedFeeRecipientAddress should return the correct forwarded fee recipient address", () => { 93 | expect(validator.getForwardedFeeRecipientAddress()).toBe(""); 94 | }); 95 | 96 | test("getEffectiveBalance should return the correct effective balance", () => { 97 | expect(validator.getEffectiveBalance()).toEqual({ 98 | amount: "100", 99 | asset: { network_id: Coinbase.networks.EthereumHoodi, asset_id: Coinbase.assets.Eth }, 100 | }); 101 | }); 102 | 103 | test("getBalance should return the correct balance", () => { 104 | expect(validator.getBalance()).toEqual({ 105 | amount: "200", 106 | asset: { network_id: Coinbase.networks.EthereumHoodi, asset_id: Coinbase.assets.Eth }, 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/tests/wallet_address_fund_test.ts: -------------------------------------------------------------------------------- 1 | import { WalletAddress } from "../coinbase/address/wallet_address"; 2 | import { FundOperation } from "../coinbase/fund_operation"; 3 | import { FundQuote } from "../coinbase/fund_quote"; 4 | import { PaginationResponse } from "../coinbase/types"; 5 | import { newAddressModel } from "./utils"; 6 | import { Decimal } from "decimal.js"; 7 | 8 | describe("WalletAddress Fund", () => { 9 | let walletAddress: WalletAddress; 10 | const walletId = "test-wallet-id"; 11 | const addressId = "0x123abc..."; 12 | 13 | beforeEach(() => { 14 | walletAddress = new WalletAddress(newAddressModel(walletId, addressId)); 15 | 16 | jest.spyOn(FundOperation, "create").mockResolvedValue({} as FundOperation); 17 | jest.spyOn(FundQuote, "create").mockResolvedValue({} as FundQuote); 18 | jest 19 | .spyOn(FundOperation, "listFundOperations") 20 | .mockResolvedValue({} as PaginationResponse); 21 | }); 22 | 23 | afterEach(() => { 24 | jest.restoreAllMocks(); 25 | }); 26 | 27 | describe("#fund", () => { 28 | it("should call FundOperation.create with correct parameters when passing in decimal amount", async () => { 29 | const amount = new Decimal("1.0"); 30 | const assetId = "eth"; 31 | 32 | await walletAddress.fund({ amount, assetId }); 33 | 34 | expect(FundOperation.create).toHaveBeenCalledWith( 35 | walletId, 36 | addressId, 37 | amount, 38 | assetId, 39 | walletAddress.getNetworkId(), 40 | ); 41 | }); 42 | it("should call FundOperation.create with correct parameters when passing in number amount", async () => { 43 | const amount = 1; 44 | const assetId = "eth"; 45 | 46 | await walletAddress.fund({ amount, assetId }); 47 | 48 | expect(FundOperation.create).toHaveBeenCalledWith( 49 | walletId, 50 | addressId, 51 | new Decimal(amount), 52 | assetId, 53 | walletAddress.getNetworkId(), 54 | ); 55 | }); 56 | it("should call FundOperation.create with correct parameters when passing in bigint amount", async () => { 57 | const amount = BigInt(1); 58 | const assetId = "eth"; 59 | 60 | await walletAddress.fund({ amount, assetId }); 61 | 62 | expect(FundOperation.create).toHaveBeenCalledWith( 63 | walletId, 64 | addressId, 65 | new Decimal(amount.toString()), 66 | assetId, 67 | walletAddress.getNetworkId(), 68 | ); 69 | }); 70 | }); 71 | 72 | describe("#quoteFund", () => { 73 | it("should call FundQuote.create with correct parameters when passing in decimal amount", async () => { 74 | const amount = new Decimal("1.0"); 75 | const assetId = "eth"; 76 | 77 | await walletAddress.quoteFund({ amount, assetId }); 78 | 79 | expect(FundQuote.create).toHaveBeenCalledWith( 80 | walletId, 81 | addressId, 82 | amount, 83 | assetId, 84 | walletAddress.getNetworkId(), 85 | ); 86 | }); 87 | it("should call FundQuote.create with correct parameters when passing in number amount", async () => { 88 | const amount = 1; 89 | const assetId = "eth"; 90 | 91 | await walletAddress.quoteFund({ amount, assetId }); 92 | 93 | expect(FundQuote.create).toHaveBeenCalledWith( 94 | walletId, 95 | addressId, 96 | new Decimal(amount), 97 | assetId, 98 | walletAddress.getNetworkId(), 99 | ); 100 | }); 101 | it("should call FundQuote.create with correct parameters when passing in bigint amount", async () => { 102 | const amount = BigInt(1); 103 | const assetId = "eth"; 104 | 105 | await walletAddress.quoteFund({ amount, assetId }); 106 | 107 | expect(FundQuote.create).toHaveBeenCalledWith( 108 | walletId, 109 | addressId, 110 | new Decimal(amount.toString()), 111 | assetId, 112 | walletAddress.getNetworkId(), 113 | ); 114 | }); 115 | }); 116 | 117 | describe("#listFundOperations", () => { 118 | it("should call listFundOperations with correct parameters", async () => { 119 | await walletAddress.listFundOperations({ limit: 10, page: "test-page" }); 120 | 121 | expect(FundOperation.listFundOperations).toHaveBeenCalledWith(walletId, addressId, { 122 | limit: 10, 123 | page: "test-page", 124 | }); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/tests/wallet_transfer_test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "../coinbase/wallet"; 2 | import { WalletAddress } from "../coinbase/address/wallet_address"; 3 | import { newAddressModel } from "./utils"; 4 | import { Coinbase, Transfer } from ".."; 5 | import { FeatureSet, Wallet as WalletModel } from "../client/api"; 6 | 7 | describe("Wallet Transfer", () => { 8 | let wallet: Wallet; 9 | let walletModel: WalletModel; 10 | let defaultAddress: WalletAddress; 11 | const walletId = "test-wallet-id"; 12 | const addressId = "0x123abc..."; 13 | 14 | beforeEach(() => { 15 | const addressModel = newAddressModel(walletId, addressId); 16 | defaultAddress = new WalletAddress(addressModel); 17 | 18 | walletModel = { 19 | id: walletId, 20 | network_id: Coinbase.networks.BaseSepolia, 21 | default_address: addressModel, 22 | feature_set: {} as FeatureSet, 23 | }; 24 | 25 | wallet = Wallet.init(walletModel, ""); 26 | 27 | // Mock getDefaultAddress to return our test address 28 | jest.spyOn(wallet, "getDefaultAddress").mockResolvedValue(defaultAddress); 29 | 30 | // Mock the createTransfer method on the default address 31 | jest.spyOn(defaultAddress, "createTransfer").mockResolvedValue({} as Transfer); 32 | }); 33 | 34 | afterEach(() => { 35 | jest.restoreAllMocks(); 36 | }); 37 | 38 | describe("#createTransfer", () => { 39 | it("should pass through skipBatching to defaultAddress.createTransfer", async () => { 40 | const assetId = "eth"; 41 | 42 | await wallet.createTransfer({ 43 | amount: 1, 44 | assetId, 45 | destination: "0x123abc...", 46 | gasless: true, 47 | skipBatching: true, 48 | }); 49 | 50 | expect(defaultAddress.createTransfer).toHaveBeenCalledWith({ 51 | amount: 1, 52 | assetId, 53 | destination: "0x123abc...", 54 | gasless: true, 55 | skipBatching: true, 56 | }); 57 | 58 | await wallet.createTransfer({ 59 | amount: 1, 60 | assetId, 61 | destination: "0x123abc...", 62 | gasless: true, 63 | skipBatching: false, 64 | }); 65 | 66 | expect(defaultAddress.createTransfer).toHaveBeenCalledWith({ 67 | amount: 1, 68 | assetId, 69 | destination: "0x123abc...", 70 | gasless: true, 71 | skipBatching: false, 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/types/calls.ts: -------------------------------------------------------------------------------- 1 | // Adapted from viem (https://github.com/wevm/viem) 2 | 3 | import type { AbiStateMutability, Address } from "viem"; 4 | import type { GetMulticallContractParameters } from "./multicall"; 5 | import type { OneOf, Prettify } from "./utils"; 6 | import type { Hex } from "./misc"; 7 | 8 | export type Call = {}> = OneOf< 9 | | (extraProperties & { 10 | data?: Hex | undefined; 11 | to: Address; 12 | value?: bigint | undefined; 13 | }) 14 | | (extraProperties & 15 | (Omit, "address"> & { 16 | to: Address; 17 | value?: bigint | undefined; 18 | })) 19 | >; 20 | 21 | export type Calls< 22 | calls extends readonly unknown[], 23 | extraProperties extends Record = {}, 24 | /// 25 | result extends readonly any[] = [], 26 | > = calls extends readonly [] // no calls, return empty 27 | ? readonly [] 28 | : calls extends readonly [infer call] // one call left before returning `result` 29 | ? readonly [...result, Prettify>] 30 | : calls extends readonly [infer call, ...infer rest] // grab first call and recurse through `rest` 31 | ? Calls<[...rest], extraProperties, [...result, Prettify>]> 32 | : readonly unknown[] extends calls 33 | ? calls 34 | : // If `calls` is *some* array but we couldn't assign `unknown[]` to it, then it must hold some known/homogenous type! 35 | // use this to infer the param types in the case of Array.map() argument 36 | calls extends readonly (infer call extends OneOf)[] 37 | ? readonly Prettify[] 38 | : // Fallback 39 | readonly OneOf[]; 40 | -------------------------------------------------------------------------------- /src/types/chain.ts: -------------------------------------------------------------------------------- 1 | import { NetworkIdentifier } from "../client/api"; 2 | 3 | /** 4 | * Maps chain IDs to their corresponding Coinbase network IDs. Only SmartWallet related chains are listed here right now. 5 | */ 6 | export const CHAIN_ID_TO_NETWORK_ID = { 7 | 8453: NetworkIdentifier.BaseMainnet, 8 | 84532: NetworkIdentifier.BaseSepolia, 9 | } as const; 10 | 11 | /** 12 | * Supported chain IDs are the keys of the CHAIN_ID_TO_NETWORK_ID object 13 | */ 14 | export type SupportedChainId = keyof typeof CHAIN_ID_TO_NETWORK_ID; 15 | 16 | /** 17 | * Represents a chainID and the corresponding Coinbase network ID 18 | */ 19 | export type Network = { 20 | chainId: SupportedChainId; 21 | networkId: NetworkIdentifier; 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/contract.ts: -------------------------------------------------------------------------------- 1 | // Adapted from viem (https://github.com/wevm/viem) 2 | 3 | import type { 4 | Abi, 5 | AbiFunction, 6 | AbiParametersToPrimitiveTypes, 7 | AbiStateMutability, 8 | Address, 9 | ExtractAbiFunction, 10 | ExtractAbiFunctionNames, 11 | ResolvedRegister, 12 | } from "abitype"; 13 | 14 | import type { Hex } from "./misc"; 15 | import type { IsUnion, UnionToTuple } from "./utils"; 16 | 17 | export type ContractFunctionName< 18 | abi extends Abi | readonly unknown[] = Abi, 19 | mutability extends AbiStateMutability = AbiStateMutability, 20 | > = 21 | ExtractAbiFunctionNames< 22 | abi extends Abi ? abi : Abi, 23 | mutability 24 | > extends infer functionName extends string 25 | ? [functionName] extends [never] 26 | ? string 27 | : functionName 28 | : string; 29 | 30 | export type ContractFunctionArgs< 31 | abi extends Abi | readonly unknown[] = Abi, 32 | mutability extends AbiStateMutability = AbiStateMutability, 33 | functionName extends ContractFunctionName = ContractFunctionName< 34 | abi, 35 | mutability 36 | >, 37 | > = 38 | AbiParametersToPrimitiveTypes< 39 | ExtractAbiFunction["inputs"], 40 | "inputs" 41 | > extends infer args 42 | ? [args] extends [never] 43 | ? readonly unknown[] 44 | : args 45 | : readonly unknown[]; 46 | 47 | export type Widen = 48 | | ([unknown] extends [type] ? unknown : never) 49 | | (type extends Function ? type : never) 50 | | (type extends ResolvedRegister["BigIntType"] ? bigint : never) 51 | | (type extends boolean ? boolean : never) 52 | | (type extends ResolvedRegister["IntType"] ? number : never) 53 | | (type extends string 54 | ? type extends ResolvedRegister["AddressType"] 55 | ? ResolvedRegister["AddressType"] 56 | : type extends ResolvedRegister["BytesType"]["inputs"] 57 | ? ResolvedRegister["BytesType"] 58 | : string 59 | : never) 60 | | (type extends readonly [] ? readonly [] : never) 61 | | (type extends Record ? { [K in keyof type]: Widen } : never) 62 | | (type extends { length: number } 63 | ? { 64 | [K in keyof type]: Widen; 65 | } extends infer Val extends readonly unknown[] 66 | ? readonly [...Val] 67 | : never 68 | : never); 69 | 70 | export type UnionWiden = type extends any ? Widen : never; 71 | 72 | export type ExtractAbiFunctionForArgs< 73 | abi extends Abi, 74 | mutability extends AbiStateMutability, 75 | functionName extends ContractFunctionName, 76 | args extends ContractFunctionArgs, 77 | > = 78 | ExtractAbiFunction extends infer abiFunction extends AbiFunction 79 | ? IsUnion extends true // narrow overloads using `args` by converting to tuple and filtering out overloads that don't match 80 | ? UnionToTuple extends infer abiFunctions extends readonly AbiFunction[] 81 | ? // convert back to union (removes `never` tuple entries) 82 | { [k in keyof abiFunctions]: CheckArgs }[number] 83 | : never 84 | : abiFunction 85 | : never; 86 | type CheckArgs< 87 | abiFunction extends AbiFunction, 88 | args, 89 | /// 90 | targetArgs extends AbiParametersToPrimitiveTypes< 91 | abiFunction["inputs"], 92 | "inputs" 93 | > = AbiParametersToPrimitiveTypes, 94 | > = (readonly [] extends args ? readonly [] : args) extends targetArgs // fallback to `readonly []` if `args` has no value (e.g. `args` property not provided) 95 | ? abiFunction 96 | : never; 97 | 98 | export type ContractFunctionParameters< 99 | abi extends Abi | readonly unknown[] = Abi, 100 | mutability extends AbiStateMutability = AbiStateMutability, 101 | functionName extends ContractFunctionName = ContractFunctionName< 102 | abi, 103 | mutability 104 | >, 105 | args extends ContractFunctionArgs = ContractFunctionArgs< 106 | abi, 107 | mutability, 108 | functionName 109 | >, 110 | deployless extends boolean = false, 111 | /// 112 | allFunctionNames = ContractFunctionName, 113 | allArgs = ContractFunctionArgs, 114 | // when `args` is inferred to `readonly []` ("inputs": []) or `never` (`abi` declared as `Abi` or not inferrable), allow `args` to be optional. 115 | // important that both branches return same structural type 116 | > = { 117 | abi: abi; 118 | functionName: 119 | | allFunctionNames // show all options 120 | | (functionName extends allFunctionNames ? functionName : never); // infer value 121 | args?: (abi extends Abi ? UnionWiden : never) | allArgs | undefined; 122 | } & (readonly [] extends allArgs ? {} : { args: Widen }) & 123 | (deployless extends true ? { address?: undefined; code: Hex } : { address: Address }); 124 | -------------------------------------------------------------------------------- /src/types/misc.ts: -------------------------------------------------------------------------------- 1 | // Adapted from viem (https://github.com/wevm/viem) 2 | 3 | export type Hex = `0x${string}`; 4 | export type Hash = `0x${string}`; 5 | export type Address = `0x${string}`; 6 | -------------------------------------------------------------------------------- /src/types/multicall.ts: -------------------------------------------------------------------------------- 1 | // Adapted from viem (https://github.com/wevm/viem) 2 | import type { Abi, AbiStateMutability } from "abitype"; 3 | 4 | import type { 5 | ContractFunctionArgs, 6 | ContractFunctionName, 7 | ContractFunctionParameters, 8 | } from "./contract"; 9 | 10 | // infer contract parameters from `unknown` 11 | export type GetMulticallContractParameters< 12 | contract, 13 | mutability extends AbiStateMutability, 14 | > = contract extends { abi: infer abi extends Abi } // 1. Check if `abi` is const-asserted or defined inline 15 | ? // 1a. Check if `functionName` is valid for `abi` 16 | contract extends { 17 | functionName: infer functionName extends ContractFunctionName; 18 | } 19 | ? // 1aa. Check if `args` is valid for `abi` and `functionName` 20 | contract extends { 21 | args: infer args extends ContractFunctionArgs; 22 | } 23 | ? ContractFunctionParameters // `args` valid, pass through 24 | : ContractFunctionParameters // invalid `args` 25 | : // 1b. `functionName` is invalid, check if `abi` is declared as `Abi` 26 | Abi extends abi 27 | ? ContractFunctionParameters // `abi` declared as `Abi`, unable to infer types further 28 | : // `abi` is const-asserted or defined inline, infer types for `functionName` and `args` 29 | ContractFunctionParameters 30 | : ContractFunctionParameters; // invalid `contract['abi']`, set to `readonly unknown[]` 31 | -------------------------------------------------------------------------------- /src/utils/chain.test.ts: -------------------------------------------------------------------------------- 1 | import { createNetwork } from "./chain"; 2 | import { CHAIN_ID_TO_NETWORK_ID, SupportedChainId } from "../types/chain"; 3 | 4 | describe("createNetwork", () => { 5 | it("should handle all supported chain IDs", () => { 6 | const supportedChainIds = Object.keys(CHAIN_ID_TO_NETWORK_ID).map(Number) as SupportedChainId[]; 7 | 8 | supportedChainIds.forEach(chainId => { 9 | const result = createNetwork(chainId); 10 | expect(result).toEqual({ 11 | chainId: chainId, 12 | networkId: CHAIN_ID_TO_NETWORK_ID[chainId], 13 | }); 14 | }); 15 | }); 16 | it("should return undefined networkId for an unsupported chain ID", () => { 17 | const result = createNetwork(1 as SupportedChainId); 18 | expect(result).toEqual({ 19 | chainId: 1, 20 | networkId: undefined, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN_ID_TO_NETWORK_ID, SupportedChainId, Network } from "../types/chain"; 2 | 3 | /** 4 | * Creates a network configuration for a given chain ID 5 | * @param chainId - The chain ID to create a network configuration for 6 | * @returns The network configuration 7 | */ 8 | export function createNetwork(chainId: SupportedChainId): Network { 9 | return { 10 | chainId, 11 | networkId: CHAIN_ID_TO_NETWORK_ID[chainId], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/wait.test.ts: -------------------------------------------------------------------------------- 1 | import { wait } from "./wait"; 2 | import { TimeoutError } from "../coinbase/errors"; 3 | 4 | describe("wait", () => { 5 | beforeEach(() => { 6 | jest.useFakeTimers(); 7 | }); 8 | 9 | afterEach(() => { 10 | jest.useRealTimers(); 11 | }); 12 | 13 | it("should resolve immediately if initial state is terminal", async () => { 14 | const mockReload = jest.fn().mockResolvedValue("COMPLETED"); 15 | const isTerminal = (status: string) => status === "COMPLETED"; 16 | 17 | const promise = wait(mockReload, isTerminal); 18 | await jest.runAllTimersAsync(); 19 | 20 | const result = await promise; 21 | expect(result).toBe("COMPLETED"); 22 | expect(mockReload).toHaveBeenCalledTimes(1); 23 | }); 24 | 25 | it("should poll until terminal state is reached", async () => { 26 | const mockReload = jest 27 | .fn() 28 | .mockResolvedValueOnce("PENDING") 29 | .mockResolvedValueOnce("PROCESSING") 30 | .mockResolvedValue("COMPLETED"); 31 | const isTerminal = (status: string) => status === "COMPLETED"; 32 | 33 | const promise = wait(mockReload, isTerminal, undefined, { 34 | intervalSeconds: 0.01, 35 | }); 36 | await jest.runAllTimersAsync(); 37 | 38 | const result = await promise; 39 | expect(result).toBe("COMPLETED"); 40 | expect(mockReload).toHaveBeenCalledTimes(3); 41 | }); 42 | 43 | it("should transform the result using provided transform function", async () => { 44 | const mockReload = jest.fn().mockResolvedValue("COMPLETED"); 45 | const isTerminal = (status: string) => status === "COMPLETED"; 46 | const transform = (status: string) => ({ status }); 47 | 48 | const promise = wait(mockReload, isTerminal, transform); 49 | await jest.runAllTimersAsync(); 50 | 51 | const result = await promise; 52 | expect(result).toEqual({ status: "COMPLETED" }); 53 | }); 54 | 55 | it("should respect custom interval", async () => { 56 | const mockReload = jest.fn().mockResolvedValueOnce("PENDING").mockResolvedValue("COMPLETED"); 57 | 58 | const isTerminal = (status: string) => status === "COMPLETED"; 59 | 60 | wait(mockReload, isTerminal, undefined, { intervalSeconds: 0.5 }); 61 | 62 | await jest.advanceTimersByTimeAsync(0); 63 | expect(mockReload).toHaveBeenCalledTimes(1); 64 | 65 | await jest.advanceTimersByTimeAsync(499); 66 | expect(mockReload).toHaveBeenCalledTimes(1); 67 | 68 | await jest.advanceTimersByTimeAsync(1); 69 | expect(mockReload).toHaveBeenCalledTimes(2); 70 | }); 71 | 72 | it("should throw TimeoutError after specified timeout", async () => { 73 | const mockReload = jest.fn().mockResolvedValue("PENDING"); 74 | const isTerminal = (status: string) => status === "COMPLETED"; 75 | 76 | const promise = wait(mockReload, isTerminal, undefined, { 77 | timeoutSeconds: 1, 78 | intervalSeconds: 0.2, 79 | }); 80 | promise.catch(error => { 81 | expect(error).toBeInstanceOf(TimeoutError); 82 | }); 83 | 84 | await jest.runAllTimersAsync(); 85 | 86 | expect(mockReload.mock.calls.length).toBeGreaterThanOrEqual(4); 87 | expect(mockReload.mock.calls.length).toBeLessThanOrEqual(6); 88 | }); 89 | 90 | it("should handle reload function failures", async () => { 91 | const mockReload = jest.fn().mockRejectedValue(new Error("Network error")); 92 | const isTerminal = (status: string) => status === "COMPLETED"; 93 | 94 | const promise = wait(mockReload, isTerminal); 95 | await expect(promise).rejects.toThrow("Network error"); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | import { TimeoutError } from "../coinbase/errors"; 2 | 3 | /** 4 | * Options for the wait function 5 | */ 6 | export type WaitOptions = { 7 | /** Interval between retries in seconds. Defaults to 0.2 */ 8 | intervalSeconds?: number; 9 | /** Maximum time to wait before timing out in seconds. Defaults to 10 */ 10 | timeoutSeconds?: number; 11 | }; 12 | 13 | /** 14 | * Polls a resource until a terminal condition is met or timeout occurs. 15 | * 16 | * @param reload - Function that fetches the latest state of the resource 17 | * @param isTerminal - Function that determines if the current state is terminal 18 | * @param transform - Function that transforms the resource into a new type 19 | * @param options - Configuration options for polling behavior 20 | * @returns The resource in its terminal state 21 | * @throws {TimeoutError} If the operation exceeds the timeout duration 22 | * 23 | * @example 24 | * const result = await wait( 25 | * () => fetchOrderStatus(orderId), 26 | * (status) => status === 'completed', 27 | * (status) => status === 'completed' ? { status } : undefined, 28 | * { timeoutSeconds: 30 } 29 | * ); 30 | */ 31 | export async function wait( 32 | reload: () => Promise, 33 | isTerminal: (obj: T) => boolean, 34 | transform: (obj: T) => K = (obj: T) => obj as unknown as K, 35 | options: WaitOptions = {}, 36 | ): Promise { 37 | const { intervalSeconds = 0.2, timeoutSeconds = 10 } = options; 38 | const startTime = Date.now(); 39 | 40 | while (Date.now() - startTime < timeoutSeconds * 1000) { 41 | const updatedObject = await reload(); 42 | 43 | if (isTerminal(updatedObject)) { 44 | return transform(updatedObject); 45 | } 46 | 47 | await new Promise(resolve => setTimeout(resolve, intervalSeconds * 1000)); 48 | } 49 | throw new TimeoutError( 50 | `Operation has not reached a terminal state after ${timeoutSeconds} seconds and may still succeed. Retry with a longer timeout using the timeoutSeconds option.`, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/wallets/createSmartWallet.test.ts: -------------------------------------------------------------------------------- 1 | import { createSmartWallet } from "./createSmartWallet"; 2 | import { Coinbase } from "../coinbase/coinbase"; 3 | import type { Address } from "../types/misc"; 4 | import { smartWalletApiMock, mockReturnValue, mockReturnRejectedValue } from "../tests/utils"; 5 | import { sendUserOperation } from "../actions/sendUserOperation"; 6 | 7 | jest.mock("../actions/sendUserOperation", () => ({ 8 | sendUserOperation: jest.fn(), 9 | })); 10 | 11 | describe("createSmartWallet", () => { 12 | const VALID_SIGNER = { 13 | address: "0x1234567890123456789012345678901234567890" as Address, 14 | sign: jest.fn(), 15 | }; 16 | 17 | const VALID_CREATE_RESPONSE = { 18 | address: "0x2234567890123456789012345678901234567890" as Address, 19 | owners: [VALID_SIGNER.address], 20 | }; 21 | 22 | beforeEach(() => { 23 | jest.clearAllMocks(); 24 | 25 | Coinbase.apiClients.smartWallet = smartWalletApiMock; 26 | Coinbase.apiClients.smartWallet!.createSmartWallet = mockReturnValue(VALID_CREATE_RESPONSE); 27 | }); 28 | 29 | afterEach(() => { 30 | jest.restoreAllMocks(); 31 | }); 32 | 33 | it("should successfully create a smart wallet", async () => { 34 | const result = await createSmartWallet({ 35 | signer: VALID_SIGNER, 36 | }); 37 | 38 | expect(Coinbase.apiClients.smartWallet!.createSmartWallet).toHaveBeenCalledWith({ 39 | owner: VALID_SIGNER.address, 40 | }); 41 | 42 | expect(result).toEqual({ 43 | address: VALID_CREATE_RESPONSE.address, 44 | owners: [VALID_SIGNER], 45 | type: "smart", 46 | sendUserOperation: expect.any(Function), 47 | useNetwork: expect.any(Function), 48 | }); 49 | }); 50 | 51 | it("should create a wallet that can send user operations", async () => { 52 | const wallet = await createSmartWallet({ 53 | signer: VALID_SIGNER, 54 | }); 55 | 56 | const operationOptions = { 57 | calls: [ 58 | { 59 | to: "0x3234567890123456789012345678901234567890" as Address, 60 | data: "0x123abc", 61 | value: 0n, 62 | }, 63 | ], 64 | chainId: 8453, 65 | } as const; 66 | 67 | await wallet.sendUserOperation(operationOptions); 68 | 69 | expect(sendUserOperation).toHaveBeenCalledWith(wallet, operationOptions); 70 | }); 71 | 72 | it("should create a wallet that can be network-scoped", async () => { 73 | const wallet = await createSmartWallet({ 74 | signer: VALID_SIGNER, 75 | }); 76 | 77 | const networkOptions = { 78 | chainId: 8453, 79 | paymasterUrl: "https://paymaster.example.com", 80 | } as const; 81 | 82 | const networkWallet = wallet.useNetwork(networkOptions); 83 | 84 | expect(networkWallet).toEqual({ 85 | ...wallet, 86 | network: expect.objectContaining({ 87 | chainId: networkOptions.chainId, 88 | }), 89 | paymasterUrl: networkOptions.paymasterUrl, 90 | sendUserOperation: expect.any(Function), 91 | }); 92 | 93 | const operationOptions = { 94 | calls: [ 95 | { 96 | to: "0x3234567890123456789012345678901234567890" as Address, 97 | data: "0x123abc", 98 | value: 0n, 99 | }, 100 | ], 101 | } as const; 102 | 103 | await networkWallet.sendUserOperation(operationOptions); 104 | 105 | expect(sendUserOperation).toHaveBeenCalledWith(wallet, { 106 | ...operationOptions, 107 | chainId: networkOptions.chainId, 108 | }); 109 | }); 110 | 111 | it("should throw if API client is not initialized", async () => { 112 | Coinbase.apiClients.smartWallet = undefined; 113 | 114 | await expect( 115 | createSmartWallet({ 116 | signer: VALID_SIGNER, 117 | }), 118 | ).rejects.toThrow(); 119 | }); 120 | 121 | it("should handle API errors during creation", async () => { 122 | Coinbase.apiClients.smartWallet!.createSmartWallet = mockReturnRejectedValue( 123 | new Error("Failed to create smart wallet"), 124 | ); 125 | 126 | await expect( 127 | createSmartWallet({ 128 | signer: VALID_SIGNER, 129 | }), 130 | ).rejects.toThrow("Failed to create smart wallet"); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/wallets/createSmartWallet.ts: -------------------------------------------------------------------------------- 1 | import { Signer, type SmartWallet } from "./types"; 2 | import { Coinbase } from "../index"; 3 | import type { Address } from "../types/misc"; 4 | import { toSmartWallet } from "./toSmartWallet"; 5 | 6 | /** 7 | * Options for creating a smart wallet 8 | */ 9 | export type CreateSmartWalletOptions = { 10 | /** The signer object that will own the smart wallet */ 11 | signer: Signer; 12 | }; 13 | 14 | /** 15 | * @description Creates a new smart wallet using the Coinbase API 16 | * 17 | * @param - {@link CreateSmartWalletOptions} options - Configuration options for creating the smart wallet 18 | * @returns {Promise} A promise that resolves to the newly created smart wallet instance 19 | * @throws {Error} If the Coinbase API client is not initialized 20 | * 21 | * See https://viem.sh/docs/accounts/local/privateKeyToAccount for using a Viem LocalAccount with SmartWallet 22 | * 23 | * @example 24 | * ```ts 25 | * import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; 26 | * import { createSmartWallet } from "@coinbase/coinbase-sdk"; 27 | * import { Coinbase } from "@coinbase/coinbase-sdk"; 28 | * 29 | * Coinbase.configureFromJson({filePath: "~/.apikeys/prod.json"}); 30 | * 31 | * const privateKey = generatePrivateKey(); 32 | * const owner = privateKeyToAccount(privateKey); 33 | * const wallet = await createSmartWallet({ 34 | * signer: owner 35 | * }); 36 | * ``` 37 | * 38 | */ 39 | export async function createSmartWallet(options: CreateSmartWalletOptions): Promise { 40 | const result = await Coinbase.apiClients.smartWallet!.createSmartWallet({ 41 | owner: options.signer.address, 42 | }); 43 | 44 | return toSmartWallet({ 45 | smartWalletAddress: result.data.address as Address, 46 | signer: options.signer, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/wallets/toSmartWallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NetworkScopedSmartWallet, 3 | Signer, 4 | SmartWalletNetworkOptions, 5 | type SmartWallet, 6 | } from "./types"; 7 | import { sendUserOperation } from "../actions/sendUserOperation"; 8 | import type { Address } from "../types/misc"; 9 | import { createNetwork } from "../utils/chain"; 10 | 11 | /** 12 | * Options for converting a smart wallet address and signer to a SmartWallet instance 13 | */ 14 | export type ToSmartWalletOptions = { 15 | /** The address of the smart wallet */ 16 | smartWalletAddress: Address; 17 | /** The signer that will own the smart wallet */ 18 | signer: Signer; 19 | }; 20 | 21 | /** 22 | * Creates a SmartWallet instance from an existing smart wallet address and signer. 23 | * Use this to interact with previously deployed smart wallets, rather than creating new ones. 24 | * 25 | * The signer must be the original owner of the smart wallet. 26 | * 27 | * @example 28 | * ```typescript 29 | * import { toSmartWallet } from "@coinbase/coinbase-sdk"; 30 | * 31 | * // Connect to an existing smart wallet 32 | * const wallet = toSmartWallet({ 33 | * smartWalletAddress: "0x1234567890123456789012345678901234567890", 34 | * signer: localAccount 35 | * }); 36 | * 37 | * // Use on a specific network 38 | * const networkWallet = wallet.useNetwork({ 39 | * chainId: 8453, // Base Mainnet 40 | * paymasterUrl: "https://paymaster.example.com" 41 | * }); 42 | * ``` 43 | * 44 | * @param {ToSmartWalletOptions} options - Configuration options 45 | * @param {string} options.smartWalletAddress - The deployed smart wallet's address 46 | * @param {Signer} options.signer - The owner's signer instance 47 | * @returns {SmartWallet} A configured SmartWallet instance ready for transaction submission 48 | * @throws {Error} If the signer is not an original owner of the wallet 49 | */ 50 | export function toSmartWallet(options: ToSmartWalletOptions): SmartWallet { 51 | const wallet: SmartWallet = { 52 | address: options.smartWalletAddress, 53 | owners: [options.signer], 54 | type: "smart", 55 | sendUserOperation: options => sendUserOperation(wallet, options), 56 | useNetwork: (options: SmartWalletNetworkOptions) => { 57 | const network = createNetwork(options.chainId); 58 | return { 59 | ...wallet, 60 | network, 61 | paymasterUrl: options.paymasterUrl, 62 | sendUserOperation: options => 63 | sendUserOperation(wallet, { 64 | ...options, 65 | chainId: network.chainId, 66 | }), 67 | } as NetworkScopedSmartWallet; 68 | }, 69 | }; 70 | 71 | return wallet; 72 | } 73 | -------------------------------------------------------------------------------- /src/wallets/types.ts: -------------------------------------------------------------------------------- 1 | import type { Hash, Hex, Address } from "../types/misc"; 2 | import type { 3 | SendUserOperationOptions, 4 | SendUserOperationReturnType, 5 | } from "../actions/sendUserOperation"; 6 | import type { Network, SupportedChainId } from "../types/chain"; 7 | import type { Prettify } from "../types/utils"; 8 | 9 | /** 10 | * Options for configuring a SmartWallet for a specific network 11 | */ 12 | export type SmartWalletNetworkOptions = { 13 | /** The chain ID of the network to connect to */ 14 | chainId: SupportedChainId; 15 | /** Optional URL for the paymaster service */ 16 | paymasterUrl?: string; 17 | }; 18 | 19 | /** 20 | * Represents a signer that can sign messages 21 | */ 22 | export type Signer = { 23 | /** The address of the signer */ 24 | address: Address; 25 | /** Signs a message hash and returns the signature as a hex string */ 26 | sign: (parameters: { hash: Hash }) => Promise; 27 | }; 28 | 29 | /** 30 | * Represents a SmartWallet with user operation capabilities 31 | */ 32 | export type SmartWallet = { 33 | /** The smart wallet's address */ 34 | address: Address; 35 | /** Array of signers that own the wallet (currently only supports one owner) */ 36 | owners: Signer[]; 37 | /** Identifier for the wallet type */ 38 | type: "smart"; 39 | /** 40 | * Sends a user operation to the network 41 | * 42 | * @param {SmartWallet} wallet - The smart wallet to send the user operation from 43 | * @param - {@link SendUserOperationOptions} options - The options for the user operation 44 | * @returns {Promise} The result of the user operation 45 | * 46 | * @example 47 | * ```ts 48 | * import { sendUserOperation } from "@coinbase/coinbase-sdk"; 49 | * import { parseEther } from "viem"; 50 | * 51 | * const result = await sendUserOperation(wallet, { 52 | * calls: [ 53 | * { 54 | * to: "0x1234567890123456789012345678901234567890", 55 | * abi: erc20Abi, 56 | * functionName: "transfer", 57 | * args: [to, amount], 58 | * }, 59 | * { 60 | * to: "0x1234567890123456789012345678901234567890", 61 | * data: "0x", 62 | * value: parseEther("0.0000005"), 63 | * }, 64 | * ], 65 | * chainId: 1, 66 | * paymasterUrl: "https://api.developer.coinbase.com/rpc/v1/base/someapikey", 67 | * }); 68 | * ``` 69 | * 70 | */ 71 | sendUserOperation: ( 72 | options: SendUserOperationOptions, 73 | ) => Promise; 74 | /** Configures the wallet for a specific network */ 75 | useNetwork: (options: SmartWalletNetworkOptions) => NetworkScopedSmartWallet; 76 | }; 77 | 78 | /** 79 | * A smart wallet that's configured for a specific network 80 | */ 81 | export type NetworkScopedSmartWallet = Prettify< 82 | Omit & { 83 | /** The network configuration */ 84 | network: Network; 85 | /** Optional URL for the paymaster service */ 86 | paymasterUrl?: string; 87 | /** Sends a user operation to the configured network */ 88 | sendUserOperation: ( 89 | options: Prettify, "chainId" | "paymasterUrl">>, 90 | ) => Promise; 91 | } 92 | >; 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": true, 14 | "noImplicitAny": false, 15 | "removeComments": false 16 | }, 17 | "include": ["src/**/*.ts"], 18 | "exclude": ["node_modules", "dist", "src/**/*.test.ts", "**/tests/**"] 19 | } 20 | --------------------------------------------------------------------------------