diff --git a/src/coinbase/address.ts b/src/coinbase/address.ts index a72e2b6a..c419fb74 100644 --- a/src/coinbase/address.ts +++ b/src/coinbase/address.ts @@ -16,7 +16,7 @@ import { ATOMIC_UNITS_PER_USDC, WEI_PER_ETHER, WEI_PER_GWEI } from "./constants" */ export class Address { private model: AddressModel; - private key: ethers.Wallet; + private key?: ethers.Wallet; /** * Initializes a new Address instance. @@ -25,14 +25,10 @@ export class Address { * @param key - The ethers.js Wallet the Address uses to sign data. * @throws {InternalError} If the model or key is empty. */ - constructor(model: AddressModel, key: ethers.Wallet) { + constructor(model: AddressModel, key?: ethers.Wallet) { if (!model) { throw new InternalError("Address model cannot be empty"); } - if (!key) { - throw new InternalError("Key cannot be empty"); - } - this.model = model; this.key = key; } @@ -76,7 +72,7 @@ export class Address { * * @returns {BalanceMap} - The map from asset ID to balance. */ - async listBalances(): Promise { + async getBalances(): Promise { const response = await Coinbase.apiClients.address!.listAddressBalances( this.model.wallet_id, this.model.address_id, @@ -85,6 +81,39 @@ export class Address { return BalanceMap.fromBalances(response.data.data); } + /** + * Returns all of the transfers associated with the address. + * + * @returns {Transfer[]} The list of transfers. + */ + async getTransfers(): Promise { + const transfers: Transfer[] = []; + const queue: string[] = [""]; + + while (queue.length > 0) { + const page = queue.shift(); + const response = await Coinbase.apiClients.transfer!.listTransfers( + this.model.wallet_id, + this.model.address_id, + 100, + page || undefined, + ); + + response.data.data.forEach(transferModel => { + transfers.push(Transfer.fromModel(transferModel)); + }); + + if (response.data.has_more) { + const nextPage = new URL(response.data.next_page).searchParams.get("starting_after"); + if (nextPage) { + queue.push(nextPage); + } + } + } + + return transfers; + } + /** * Returns the balance of the provided asset. * @@ -115,11 +144,11 @@ export class Address { } /** - * Sends an amount of an asset to a destination. + * Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported. * - * @param amount - The amount to send. - * @param assetId - The asset ID to send. - * @param destination - The destination address. + * @param amount - The amount of the Asset to send. + * @param assetId - The ID of the Asset to send. For Ether, Coinbase.assetList.Eth, Coinbase.assetList.Gwei, and Coinbase.assetList.Wei supported. + * @param destination - The destination of the transfer. If a Wallet, sends to the Wallet's default address. If a String, interprets it as the address ID. * @param intervalSeconds - The interval at which to poll the Network for Transfer status, in seconds. * @param timeoutSeconds - The maximum amount of time to wait for the Transfer to complete, in seconds. * @returns The transfer object. @@ -134,8 +163,10 @@ export class Address { intervalSeconds = 0.2, timeoutSeconds = 10, ): Promise { + if (!this.key) { + throw new InternalError("Cannot transfer from address without private key loaded"); + } let normalizedAmount = new Decimal(amount.toString()); - const currentBalance = await this.getBalance(assetId); if (currentBalance.lessThan(normalizedAmount)) { throw new ArgumentError( diff --git a/src/coinbase/tests/address_test.ts b/src/coinbase/tests/address_test.ts index 79cb47ed..dea4b1a7 100644 --- a/src/coinbase/tests/address_test.ts +++ b/src/coinbase/tests/address_test.ts @@ -2,7 +2,7 @@ import { Address } from "./../address"; import * as crypto from "crypto"; import { ethers } from "ethers"; import { FaucetTransaction } from "./../faucet_transaction"; -import { Balance as BalanceModel } from "../../client"; +import { Balance as BalanceModel, TransferList } from "../../client"; import Decimal from "decimal.js"; import { APIError, FaucetLimitReachedError } from "../api_error"; import { Coinbase } from "../coinbase"; @@ -68,7 +68,7 @@ describe("Address", () => { }); it("should return the correct list of balances", async () => { - const balances = await address.listBalances(); + const balances = await address.getBalances(); expect(balances.get(Coinbase.assetList.Eth)).toEqual(new Decimal(1)); expect(balances.get("usdc")).toEqual(new Decimal(5000)); expect(balances.get("weth")).toEqual(new Decimal(3)); @@ -305,4 +305,40 @@ describe("Address", () => { jest.restoreAllMocks(); }); }); + + describe(".getTransfers", () => { + beforeEach(() => { + jest.clearAllMocks(); + const pages = ["http://localhost?starting_after=abc", "http://localhost?starting_after=def"]; + const response = { + data: [VALID_TRANSFER_MODEL], + has_more: false, + next_page: "", + total_count: 0, + } as TransferList; + Coinbase.apiClients.transfer!.listTransfers = mockFn(() => { + response.next_page = pages.shift() as string; + response.data = [VALID_TRANSFER_MODEL]; + response.has_more = !!response.next_page; + return { data: response }; + }); + }); + it("should return the list of transfers", async () => { + const transfers = await address.getTransfers(); + expect(transfers).toHaveLength(3); + expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledTimes(3); + expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + 100, + undefined, + ); + expect(Coinbase.apiClients.transfer!.listTransfers).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + 100, + "abc", + ); + }); + }); }); diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts index 4d737231..8bfbccf1 100644 --- a/src/coinbase/tests/user_test.ts +++ b/src/coinbase/tests/user_test.ts @@ -14,11 +14,15 @@ import { User } from "./../user"; import { Wallet } from "./../wallet"; import { addressesApiMock, + getAddressFromHDKey, mockReturnRejectedValue, mockReturnValue, newAddressModel, walletsApiMock, } from "./utils"; +import { HDKey } from "@scure/bip32"; +import { convertStringToHex } from "../utils"; +import { UnhydratedWallet } from "../unhydrated_wallet"; describe("User Class", () => { let mockUserModel: UserModel; @@ -51,7 +55,11 @@ describe("User Class", () => { beforeAll(async () => { walletId = crypto.randomUUID(); - mockAddressModel = newAddressModel(walletId); + walletData = { walletId: walletId, seed: bip39.generateMnemonic() }; + const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(walletData.seed)); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); + const address1 = getAddressFromHDKey(wallet1); + mockAddressModel = newAddressModel(walletId, address1); mockAddressList = { data: [mockAddressModel], has_more: false, @@ -68,7 +76,6 @@ describe("User Class", () => { Coinbase.apiClients.address = addressesApiMock; Coinbase.apiClients.address!.listAddresses = mockReturnValue(mockAddressList); user = new User(mockUserModel); - walletData = { walletId: walletId, seed: bip39.generateMnemonic() }; importedWallet = await user.importWallet(walletData); expect(importedWallet).toBeInstanceOf(Wallet); expect(Coinbase.apiClients.wallet!.getWallet).toHaveBeenCalledWith(walletId); @@ -99,10 +106,14 @@ describe("User Class", () => { beforeAll(async () => { walletId = crypto.randomUUID(); seed = "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe"; + const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(seed)); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); + const address1 = getAddressFromHDKey(wallet1); + mockAddressModel = { - address_id: "0xdeadbeef", + address_id: address1, wallet_id: walletId, - public_key: "0x1234567890", + public_key: convertStringToHex(wallet1.privateKey!), network_id: Coinbase.networkList.BaseSepolia, }; mockWalletModel = { @@ -170,6 +181,7 @@ describe("User Class", () => { let seedDataWithoutSeed: Record; let seedDataWithoutIv: Record; let seedDataWithoutAuthTag: Record; + let seed: string = "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe"; beforeAll(() => { walletId = crypto.randomUUID(); @@ -199,7 +211,7 @@ describe("User Class", () => { initialSeedData = { [walletId]: { - seed: "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe", + seed, encrypted: false, iv: "", authTag: "", @@ -216,7 +228,7 @@ describe("User Class", () => { }; seedDataWithoutIv = { [walletId]: { - seed: "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe", + seed, encrypted: true, iv: "", auth_tag: "0x111", @@ -224,7 +236,7 @@ describe("User Class", () => { }; seedDataWithoutAuthTag = { [walletId]: { - seed: "86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe", + seed, encrypted: true, iv: "0x111", auth_tag: "", @@ -244,6 +256,27 @@ describe("User Class", () => { }); it("loads the Wallet from backup", async () => { + const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(seed)); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); + const address1 = getAddressFromHDKey(wallet1); + + const wallet2 = baseWallet.derive("m/44'/60'/0'/0/1"); + const address2 = getAddressFromHDKey(wallet2); + + const addressModel1: AddressModel = newAddressModel(walletId, address1); + const addressModel2: AddressModel = newAddressModel(walletId, address2); + walletModelWithDefaultAddress = { + id: walletId, + network_id: Coinbase.networkList.BaseSepolia, + default_address: addressModel1, + }; + addressListModel = { + data: [addressModel1, addressModel2], + has_more: false, + next_page: "", + total_count: 2, + }; + Coinbase.apiClients.wallet = walletsApiMock; Coinbase.apiClients.wallet!.getWallet = mockReturnValue(walletModelWithDefaultAddress); Coinbase.apiClients.address = addressesApiMock; @@ -253,7 +286,7 @@ describe("User Class", () => { const wallet = wallets[walletId]; expect(wallet).not.toBeNull(); expect(wallet.getId()).toBe(walletId); - expect(wallet.getDefaultAddress()?.getId()).toBe(addressModel.address_id); + expect(wallet.getDefaultAddress()?.getId()).toBe(addressModel1.address_id); }); it("throws an error when the backup file is absent", async () => { @@ -291,6 +324,12 @@ describe("User Class", () => { beforeEach(() => { jest.clearAllMocks(); walletId = crypto.randomUUID(); + const seed = bip39.generateMnemonic(); + const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(seed)); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); + const address1 = getAddressFromHDKey(wallet1); + mockAddressModel = newAddressModel(walletId, address1); + const addressModel1: AddressModel = newAddressModel(walletId); const addressModel2: AddressModel = newAddressModel(walletId); walletModelWithDefaultAddress = { @@ -347,14 +386,22 @@ describe("User Class", () => { }); it("should return the list of Wallets", async () => { + const seed = bip39.generateMnemonic(); + const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(seed)); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); + const address1 = getAddressFromHDKey(wallet1); + mockAddressModel = newAddressModel(walletId, address1); + Coinbase.apiClients.wallet!.listWallets = mockReturnValue({ data: [walletModelWithDefaultAddress], has_more: false, next_page: "", - total_count: 2, + total_count: 1, }); Coinbase.apiClients.address!.listAddresses = mockReturnValue(addressListModel); const wallets = await user.getWallets(); + // instance of UnhydratedWallet + expect(wallets[0]).toBeInstanceOf(UnhydratedWallet); expect(wallets.length).toBe(1); expect(wallets[0].getId()).toBe(walletId); expect(wallets[0].getAddresses().length).toBe(2); diff --git a/src/coinbase/tests/utils.ts b/src/coinbase/tests/utils.ts index 5493b2c4..e397d8bf 100644 --- a/src/coinbase/tests/utils.ts +++ b/src/coinbase/tests/utils.ts @@ -38,11 +38,11 @@ export const generateRandomHash = (length = 8) => { }; // newAddressModel creates a new AddressModel with a random wallet ID and a random Ethereum address. -export const newAddressModel = (walletId: string): AddressModel => { +export const newAddressModel = (walletId: string, address_id: string = ""): AddressModel => { const ethAddress = ethers.Wallet.createRandom(); return { - address_id: ethAddress.address, + address_id: address_id ? address_id : ethAddress.address, network_id: Coinbase.networkList.BaseSepolia, public_key: ethAddress.publicKey, wallet_id: walletId, diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index e89190e2..e15e33b0 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -27,6 +27,7 @@ import { } from "./utils"; import { APIError } from "../api_error"; import { GWEI_PER_ETHER, WEI_PER_ETHER } from "../constants"; +import { convertStringToHex } from "../utils"; describe("Wallet Class", () => { let wallet: Wallet; @@ -36,6 +37,7 @@ describe("Wallet Class", () => { beforeAll(async () => { walletId = crypto.randomUUID(); + // Mock the API calls Coinbase.apiClients.wallet = walletsApiMock; Coinbase.apiClients.address = addressesApiMock; @@ -230,28 +232,35 @@ describe("Wallet Class", () => { }); describe(".init", () => { - walletId = crypto.randomUUID(); + let wallet: Wallet; + let walletId = crypto.randomUUID(); + const existingSeed = "hidden assault maple cheap gentle paper earth surprise trophy guide room tired"; const baseWallet = HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(existingSeed)); - const wallet1 = baseWallet.deriveChild(0); + const wallet1 = baseWallet.derive("m/44'/60'/0'/0/0"); const address1 = getAddressFromHDKey(wallet1); - const wallet2 = baseWallet.deriveChild(1); + const wallet2 = baseWallet.derive("m/44'/60'/0'/0/1"); const address2 = getAddressFromHDKey(wallet2); const addressList = [ { address_id: address1, network_id: Coinbase.networkList.BaseSepolia, - public_key: "0x032c11a826d153bb8cf17426d03c3ffb74ea445b17362f98e1536f22bcce720772", + public_key: convertStringToHex(wallet1.privateKey!), wallet_id: walletId, }, { address_id: address2, network_id: Coinbase.networkList.BaseSepolia, - public_key: "0x03c3379b488a32a432a4dfe91cc3a28be210eddc98b2005bb59a4cf4ed0646eb56", + public_key: convertStringToHex(wallet2.privateKey!), wallet_id: walletId, }, ]; + const walletModel: WalletModel = { + id: walletId, + network_id: Coinbase.networkList.BaseSepolia, + default_address: VALID_ADDRESS_MODEL, + }; beforeEach(async () => { jest.clearAllMocks(); @@ -261,41 +270,6 @@ describe("Wallet Class", () => { it("should return a Wallet instance", async () => { expect(wallet).toBeInstanceOf(Wallet); }); - - it("should return the correct wallet ID", async () => { - expect(wallet.getId()).toBe(walletModel.id); - }); - - it("should return the correct network ID", async () => { - expect(wallet.getNetworkId()).toBe(Coinbase.networkList.BaseSepolia); - }); - - it("should derive the correct number of addresses", async () => { - expect(wallet.getAddresses().length).toBe(2); - }); - - it("should derive the correct addresses", async () => { - const addresses = wallet.getAddresses(); - expect(wallet.getAddress(address1)).toBe(addresses[0]); - expect(wallet.getAddress(address2)).toBe(addresses[1]); - }); - - it("should create new address and update the existing address list", async () => { - const newAddress = await wallet.createAddress(); - expect(newAddress).toBeInstanceOf(Address); - expect(wallet.getAddresses().length).toBe(3); - expect(wallet.getAddress(newAddress.getId())!.getId()).toBe(newAddress.getId()); - }); - - it("should return the correct string representation", async () => { - expect(wallet.toString()).toBe( - `Wallet{id: '${walletModel.id}', networkId: '${Coinbase.networkList.BaseSepolia}'}`, - ); - }); - - it("should throw an ArgumentError when the wallet model is not provided", async () => { - await expect(Wallet.init(undefined!)).rejects.toThrow(ArgumentError); - }); }); describe(".export", () => { diff --git a/src/coinbase/unhydrated_wallet.ts b/src/coinbase/unhydrated_wallet.ts new file mode 100644 index 00000000..dad3bbff --- /dev/null +++ b/src/coinbase/unhydrated_wallet.ts @@ -0,0 +1,151 @@ +import Decimal from "decimal.js"; +import { Address as AddressModel, Wallet as WalletModel } from "../client"; +import { Address } from "./address"; +import { Balance } from "./balance"; +import { BalanceMap } from "./balance_map"; +import { Coinbase } from "./coinbase"; +import { FaucetTransaction } from "./faucet_transaction"; +import { Wallet } from "./wallet"; +import { InternalError } from "./errors"; + +/** + * Represents a Wallet that has not been hydrated with a seed. + */ +export class UnhydratedWallet { + protected model: WalletModel; + protected addresses: Address[] = []; + protected addressModels: AddressModel[] = []; + + /** + * Initializes a new UnhydratedWallet instance. + * + * @param wallet - The Wallet model. + * @param addressModels - The list of Address models. + */ + constructor(wallet: WalletModel, addressModels: AddressModel[]) { + this.model = wallet; + this.addressModels = addressModels; + this.addresses = this.addressModels.map(addressModel => { + return new Address(addressModel); + }); + } + + /** + * Returns the Wallet model. + * + * @param seed - The seed to use for the Wallet. Expects a 32-byte hexadecimal with no 0x prefix. + * @returns The Wallet object. + */ + public async setSeed(seed: string): Promise { + return await Wallet.init(this.model, seed, this.addressModels); + } + + /** + * Returns the Address with the given ID. + * + * @param addressId - The ID of the Address to retrieve. + * @returns The Address. + */ + public getAddress(addressId: string): Address | undefined { + return this.addresses.find(address => { + return address.getId() === addressId; + }); + } + + /** + * Requests funds from the faucet for the Wallet's default address and returns the faucet transaction. + * This is only supported on testnet networks. + * + * @throws {InternalError} If the default address is not found. + * @throws {APIError} If the request fails. + * @returns The successful faucet transaction + */ + public async faucet(): Promise { + if (!this.model.default_address) { + throw new InternalError("Default address not found"); + } + const transaction = await this.getDefaultAddress()!.faucet(); + return transaction!; + } + + /** + * Returns the list of Addresses in the Wallet. + * + * @returns The list of Addresses. + */ + public getAddresses(): Address[] { + return this.addresses; + } + + /** + * Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet. + * + * @returns The list of balances. The key is the Asset ID, and the value is the balance. + */ + public async getBalances(): Promise { + const response = await Coinbase.apiClients.wallet!.listWalletBalances(this.model.id!); + return BalanceMap.fromBalances(response.data.data); + } + + /** + * Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet. + * + * @param assetId - The ID of the Asset to retrieve the balance for. + * @returns The balance of the Asset. + */ + public async getBalance(assetId: string): Promise { + const response = await Coinbase.apiClients.wallet!.getWalletBalance(this.model.id!, assetId); + if (!response.data.amount) { + return new Decimal(0); + } + const balance = Balance.fromModelAndAssetId(response.data, assetId); + return balance.amount; + } + + /** + * Returns the Network ID of the Wallet. + * + * @returns The network ID. + */ + public getNetworkId(): string { + return this.model.network_id; + } + + /** + * Returns the wallet ID. + * + * @returns The wallet ID. + */ + public getId(): string | undefined { + return this.model.id; + } + + /** + * Returns the default address of the Wallet. + * + * @returns The default address + */ + public getDefaultAddress(): Address | undefined { + return this.addresses.find( + address => address.getId() === this.model.default_address?.address_id, + ); + } + + /** + * Returns whether the Wallet has a seed with which to derive keys and sign transactions. + * + * @returns Whether the Wallet has a seed with which to derive keys and sign transactions. + */ + public canSign(): boolean { + return false; + } + + /** + * Returns a String representation of the UnhydratedWallet. + * + * @returns a String representation of the UnhydratedWallet + */ + public toString(): string { + return `UnhydratedWallet{id: '${this.model.id}', networkId: '${this.model.network_id}'}`; + } +} diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index 5501626d..24b20d31 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -5,6 +5,7 @@ import { User as UserModel, Address as AddressModel, Wallet as WalletModel } fro import { Wallet } from "./wallet"; import { Coinbase } from "./coinbase"; import { ArgumentError } from "./errors"; +import { UnhydratedWallet } from "./unhydrated_wallet"; /** * A representation of a User. @@ -97,7 +98,10 @@ export class User { * @param nextPageToken - The token for the next page of Wallets * @returns The list of Wallets. */ - public async getWallets(pageSize: number = 10, nextPageToken: string = ""): Promise { + public async getWallets( + pageSize: number = 10, + nextPageToken: string = "", + ): Promise { const addressModelMap: { [key: string]: AddressModel[] } = {}; const walletList = await Coinbase.apiClients.wallet!.listWallets(pageSize, nextPageToken); const walletsModels: WalletModel[] = []; @@ -113,11 +117,9 @@ export class User { addressModelMap[wallet.id!] = addressList.data.data; } - return await Promise.all( - walletsModels.map(async wallet => { - return await Wallet.init(wallet, "", addressModelMap[wallet.id!]); - }), - ); + return walletsModels.map(wallet => { + return new UnhydratedWallet(wallet, addressModelMap[wallet.id!]); + }); } /** diff --git a/src/coinbase/wallet.ts b/src/coinbase/wallet.ts index 443cec0d..59362325 100644 --- a/src/coinbase/wallet.ts +++ b/src/coinbase/wallet.ts @@ -6,27 +6,20 @@ import * as secp256k1 from "secp256k1"; import { Address as AddressModel, Wallet as WalletModel } from "../client"; import { Address } from "./address"; import { Coinbase } from "./coinbase"; -import { Transfer } from "./transfer"; import { ArgumentError, InternalError } from "./errors"; -import { FaucetTransaction } from "./faucet_transaction"; +import { Transfer } from "./transfer"; import { Amount, Destination, WalletData } from "./types"; +import { UnhydratedWallet } from "./unhydrated_wallet"; import { convertStringToHex } from "./utils"; -import { BalanceMap } from "./balance_map"; -import Decimal from "decimal.js"; -import { Balance } from "./balance"; /** * A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses, * each of which can hold a balance of one or more Assets. Wallets can create new Addresses, list their addresses, * list their balances, and transfer Assets to other Addresses. Wallets should be created through User.createWallet or User.importWallet. */ -export class Wallet { - private model: WalletModel; - +export class Wallet extends UnhydratedWallet { private master: HDKey; private seed: string; - private addresses: Address[] = []; - private addressModels: AddressModel[] = []; private readonly addressPathPrefix = "m/44'/60'/0'/0"; private addressIndex = 0; static MAX_ADDRESSES = 20; @@ -47,6 +40,7 @@ export class Wallet { seed: string, addressModels: AddressModel[] = [], ) { + super(model, addressModels); this.model = model; this.master = master; this.seed = seed; @@ -118,20 +112,6 @@ export class Wallet { return { walletId: this.getId()!, seed: this.seed }; } - /** - * Derives a key for an already registered Address in the Wallet. - * - * @throws {InternalError} - If the key derivation fails. - * @returns The derived key. - */ - private deriveKey(): HDKey { - const derivedKey = this.master.derive(`${this.addressPathPrefix}/${this.addressIndex++}`); - if (!derivedKey?.privateKey) { - throw new InternalError("Failed to derive key"); - } - return derivedKey; - } - /** * Creates a new Address in the Wallet. * @@ -193,6 +173,20 @@ export class Wallet { this.model = result?.data; } + /** + * Derives a key for an already registered Address in the Wallet. + * + * @throws {InternalError} - If the key derivation fails. + * @returns The derived key. + */ + private deriveKey(): HDKey { + const derivedKey = this.master.derive(this.addressPathPrefix + "/" + this.addressIndex); + if (!derivedKey?.privateKey) { + throw new InternalError("Failed to derive key"); + } + return derivedKey; + } + /** * Derives an already registered Address in the Wallet. * @@ -200,15 +194,11 @@ export class Wallet { * @param addressModel - The Address model * @throws {InternalError} - If address derivation fails. * @throws {APIError} - If the request fails. - * @returns A promise that resolves when the address is derived. */ - private async deriveAddress( - addressMap: { [key: string]: boolean }, - addressModel: AddressModel, - ): Promise { + private deriveAddress(addressMap: { [key: string]: boolean }, addressModel: AddressModel): void { const hdKey = this.deriveKey(); const key = new ethers.Wallet(convertStringToHex(hdKey.privateKey!)); - if (addressMap[key.address]) { + if (!addressMap[key.address.toString()]) { throw new InternalError("Invalid address"); } @@ -220,7 +210,7 @@ export class Wallet { * * @param addresses - The models of the addresses already registered with the */ - public async deriveAddresses(addresses: AddressModel[]): Promise { + public deriveAddresses(addresses: AddressModel[]): void { const addressMap = this.buildAddressMap(addresses); for (const address of addresses) { this.deriveAddress(addressMap, address); @@ -253,82 +243,6 @@ export class Wallet { */ private cacheAddress(address: AddressModel, key: ethers.Wallet): void { this.addresses.push(new Address(address, key)); - this.addressIndex++; - } - - /** - * Returns the Address with the given ID. - * - * @param addressId - The ID of the Address to retrieve. - * @returns The Address. - */ - public getAddress(addressId: string): Address | undefined { - return this.addresses.find(address => { - return address.getId() === addressId; - }); - } - - /** - * Returns the list of Addresses in the Wallet. - * - * @returns The list of Addresses. - */ - public getAddresses(): Address[] { - return this.addresses; - } - - /** - * Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet. - * - * @returns The list of balances. The key is the Asset ID, and the value is the balance. - */ - public async getBalances(): Promise { - const response = await Coinbase.apiClients.wallet!.listWalletBalances(this.model.id!); - return BalanceMap.fromBalances(response.data.data); - } - - /** - * Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet. - * - * @param assetId - The ID of the Asset to retrieve the balance for. - * @returns The balance of the Asset. - */ - public async getBalance(assetId: string): Promise { - const response = await Coinbase.apiClients.wallet!.getWalletBalance(this.model.id!, assetId); - if (!response.data.amount) { - return new Decimal(0); - } - const balance = Balance.fromModelAndAssetId(response.data, assetId); - return balance.amount; - } - - /** - * Returns the Network ID of the Wallet. - * - * @returns The network ID. - */ - public getNetworkId(): string { - return this.model.network_id; - } - - /** - * Returns the wallet ID. - * - * @returns The wallet ID. - */ - public getId(): string | undefined { - return this.model.id; - } - - /** - * Returns the default address of the Wallet. - * - * @returns The default address - */ - public getDefaultAddress(): Address | undefined { - return this.addresses.find( - address => address.getId() === this.model.default_address?.address_id, - ); } /** @@ -340,22 +254,6 @@ export class Wallet { return this.master.publicKey !== undefined; } - /** - * Requests funds from the faucet for the Wallet's default address and returns the faucet transaction. - * This is only supported on testnet networks. - * - * @throws {InternalError} If the default address is not found. - * @throws {APIError} If the request fails. - * @returns The successful faucet transaction - */ - public async faucet(): Promise { - if (!this.model.default_address) { - throw new InternalError("Default address not found"); - } - const transaction = await this.getDefaultAddress()!.faucet(); - return transaction!; - } - /** * Sends an amount of an asset to a destination. *