diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 00000000..1fd2affb --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,22 @@ +name: Run E2E Tests + +on: [pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Run E2E Tests + env: + NAME: ${{ secrets.NAME }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + WALLET_DATA: ${{ secrets.WALLET_DATA }} + run: npm run test:dry-run && npm run test:e2e diff --git a/README.md b/README.md index bbaea319..ea817715 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ To start, [create a CDP API Key](https://portal.cdp.coinbase.com/access/api). Th ```typescript const apiKeyName = "Copy your API Key name here."; -const privatekey = "Copy your API Key's private key here."; +const privateKey = "Copy your API Key's private key here."; const coinbase = new Coinbase({ apiKeyName: apiKeyName, privateKey: privateKey }); ``` @@ -165,16 +165,16 @@ In order to persist the data for the Wallet, you will need to implement a store await store(data); ``` -For convenience during testing, we provide a `saveWallet` method that stores the Wallet data in your local file system. This is an insecure method of storing wallet seeds and should only be used for development purposes. +For convenience during testing, we provide a `saveSeed` method that stores the wallet's seed in your local file system. This is an insecure method of storing wallet seeds and should only be used for development purposes. ```typescript -user.saveWallet(wallet); +wallet.saveSeed(wallet); ``` -To encrypt the saved data, set encrypt to true. Note that your CDP API key also serves as the encryption key for the data persisted locally. To re-instantiate wallets with encrypted data, ensure that your SDK is configured with the same API key when invoking `saveWallet` and `loadWallets`. +To encrypt the saved data, set encrypt to true. Note that your CDP API key also serves as the encryption key for the data persisted locally. To re-instantiate wallets with encrypted data, ensure that your SDK is configured with the same API key when invoking `saveSeed` and `loadSeed`. ```typescript -user.saveWallet(wallet, true); +wallet.saveSeed(wallet, true); ``` The below code demonstrates how to re-instantiate a Wallet from the data export. @@ -184,12 +184,12 @@ The below code demonstrates how to re-instantiate a Wallet from the data export. const importedWallet = await user.importWallet(data); ``` -To import Wallets that were persisted to your local file system using `saveWallet`, use the below code. +To import Wallets that were persisted to your local file system using `saveSeed`, use the below code. ```typescript // The Wallet can be re-instantiated using the exported data. -const wallets = await user.loadWallets(); -const reinitWallet = wallets[wallet.getId()]; +const w = await user.getWallet(w.getId()); +w.loadSeed(filePath); ``` ## Development @@ -239,6 +239,11 @@ To run a specific test, run (for example): ```bash npx jest ./src/coinbase/tests/wallet_test.ts ``` +To run e2e tests, run: + +```bash +npm run test:dry-run && NAME="placeholder" PRIVATE_KEY="placeholder" WALLET_DATA="placeholder" && npm run test:e2e +``` ### Generating Documentation diff --git a/package.json b/package.json index c818a4bb..68e87a2e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", "format-check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", "check": "tsc --noEmit", - "test": "npx jest --no-cache", + "test": "npx jest --no-cache --testMatch=**/*_test.ts", + "test:dry-run": "npm install && npm ci && npm publish --dry-run", + "test:e2e": "npx jest --no-cache --testMatch=**/e2e.ts", "clean": "rm -rf dist/*", "build": "tsc", "prepack": "tsc", @@ -28,6 +30,7 @@ "bip32": "^4.0.0", "bip39": "^3.1.0", "decimal.js": "^10.4.3", + "dotenv": "^16.4.5", "ethers": "^6.12.1", "node-jose": "^2.2.0", "secp256k1": "^5.0.0" @@ -69,7 +72,6 @@ "text" ], "verbose": true, - "testRegex": ".test.ts$", "maxWorkers": 1 } } diff --git a/src/coinbase/tests/e2e.ts b/src/coinbase/tests/e2e.ts new file mode 100644 index 00000000..ab748929 --- /dev/null +++ b/src/coinbase/tests/e2e.ts @@ -0,0 +1,112 @@ +import fs from "fs"; +import dotenv from "dotenv"; +import { Coinbase } from "../coinbase"; +import { TransferStatus } from "../types"; + +describe("Coinbase SDK E2E Test", () => { + let coinbase: Coinbase; + beforeAll(() => { + dotenv.config(); + }); + + beforeEach(() => { + coinbase = new Coinbase({ + apiKeyName: process.env.NAME, + privateKey: process.env.PRIVATE_KEY, + }); + }); + + it("should be able to access environment variables", () => { + expect(process.env.NAME).toBeDefined(); + expect(process.env.PRIVATE_KEY).toBeDefined(); + }); + + it("should have created a dist folder for NPM", () => { + expect(fs.existsSync("./dist")).toBe(true); + expect(fs.existsSync("./dist/index.js")).toBe(true); + expect(fs.existsSync("./dist/client/index.js")).toBe(true); + expect(fs.existsSync("./dist/coinbase/coinbase.js")).toBe(true); + }); + + it("should be able to interact with the Coinbase SDK", async () => { + console.log("Fetching default user..."); + const user = await coinbase.getDefaultUser(); + expect(user.getId()).toBeDefined(); + console.log(`Fetched default user with ID: ${user.getId()}`); + + console.log("Creating new wallet..."); + const wallet = await user.createWallet(); + expect(wallet?.getId()).toBeDefined(); + console.log( + `Created new wallet with ID: ${wallet.getId()}, default address: ${wallet.getDefaultAddress()}`, + ); + + console.log("Importing wallet with balance..."); + const seedFile = JSON.parse(process.env.WALLET_DATA || ""); + const walletId = Object.keys(seedFile)[0]; + const seed = seedFile[walletId].seed; + + const userWallet = await user.importWallet({ seed, walletId }); + expect(userWallet).toBeDefined(); + expect(userWallet.getId()).toBe(walletId); + console.log( + `Imported wallet with ID: ${userWallet.getId()}, default address: ${userWallet.getDefaultAddress()}`, + ); + await userWallet.saveSeed("test_seed.json"); + + try { + await userWallet.faucet(); + } catch { + console.log("Faucet request failed. Skipping..."); + } + console.log("Listing wallet addresses..."); + const addresses = userWallet.listAddresses(); + expect(addresses.length).toBeGreaterThan(0); + console.log(`Listed addresses: ${userWallet.listAddresses().join(", ")}`); + + console.log("Fetching wallet balances..."); + const balances = await userWallet.listBalances(); + expect(Array.from([...balances.keys()]).length).toBeGreaterThan(0); + console.log(`Fetched balances: ${balances.toString()}`); + + console.log("Exporting wallet..."); + const exportedWallet = await wallet.export(); + expect(exportedWallet.walletId).toBeDefined(); + expect(exportedWallet.seed).toBeDefined(); + + console.log("Saving seed to file..."); + await wallet.saveSeed("test_seed.json"); + expect(fs.existsSync("test_seed.json")).toBe(true); + console.log("Saved seed to test_seed.json"); + + const unhydratedWallet = await user.getWallet(walletId); + expect(unhydratedWallet.canSign()).toBe(false); + await unhydratedWallet.loadSeed("test_seed.json"); + expect(unhydratedWallet.canSign()).toBe(true); + expect(unhydratedWallet.getId()).toBe(walletId); + + console.log("Transfering 1 Gwei from default address to second address..."); + const transfer = await unhydratedWallet.createTransfer(1, Coinbase.assets.Gwei, wallet); + expect(await transfer.getStatus()).toBe(TransferStatus.COMPLETE); + console.log(`Transferred 1 Gwei from ${unhydratedWallet} to ${wallet}`); + + console.log("Fetching updated balances..."); + const firstBalance = await unhydratedWallet.listBalances(); + const secondBalance = await wallet.listBalances(); + expect(firstBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); + expect(secondBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); + console.log(`First address balances: ${firstBalance}`); + console.log(`Second address balances: ${secondBalance}`); + + const savedSeed = JSON.parse(fs.readFileSync("test_seed.json", "utf-8")); + fs.unlinkSync("test_seed.json"); + + expect(exportedWallet.seed.length).toBe(64); + expect(savedSeed[exportedWallet.walletId]).toEqual({ + seed: exportedWallet.seed, + encrypted: false, + authTag: "", + iv: "", + }); + }, 60000); +}); diff --git a/src/coinbase/wallet.ts b/src/coinbase/wallet.ts index 197d9bda..c8bb3962 100644 --- a/src/coinbase/wallet.ts +++ b/src/coinbase/wallet.ts @@ -307,6 +307,17 @@ export class Wallet { public setSeed(seed: string) { if (this.master === undefined && (this.seed === undefined || this.seed === "")) { this.master = HDKey.fromMasterSeed(Buffer.from(seed, "hex")); + this.addresses = []; + this.addressModels.map((addressModel: AddressModel) => { + const derivedKey = this.deriveKey(); + const etherKey = new ethers.Wallet(convertStringToHex(derivedKey.privateKey!)); + if (etherKey.address != addressModel.address_id) { + throw new InternalError( + `Seed does not match wallet; cannot find address ${etherKey.address}`, + ); + } + this.cacheAddress(addressModel, etherKey); + }); } else { throw new InternalError("Cannot set seed on Wallet with existing seed"); } diff --git a/yarn.lock b/yarn.lock index 000b7cfa..395a7e5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,6 +1466,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + electron-to-chromium@^1.4.668: version "1.4.773" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.773.tgz#49741af9bb4e712ad899e35d8344d8d59cdb7e12"