Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Coinbase Node.js SDK Changelog

## [0.14.0] - 2025-01-14

### Added
- Add `skipBatching` option to `Wallet.createTransfer` to allow for lower latency gasless transfers.

## [0.13.0] - 2024-12-19

### Added
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ let transfer = await wallet.createTransfer({ amount: 0.00001, assetId: Coinbase.
transfer = await transfer.wait();
```

By default, gasless transfers are batched with other transfers, and might take longer to submit. If you want to opt out of batching, you can set the `skipBatching` option to `true`, which will submit the transaction immediately.
```typescript
let transfer = await wallet.createTransfer({
amount: 0.00001,
assetId: Coinbase.assets.Usdc,
destination: anotherWallet,
gasless: true,
skipBatching: true
});
transfer = await transfer.wait();
```

### Trading Funds

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "ISC",
"description": "Coinbase Platform SDK",
"repository": "https://github.com/coinbase/coinbase-sdk-nodejs",
"version": "0.13.0",
"version": "0.14.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion quickstart-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dependencies": {
"@solana/web3.js": "^2.0.0-rc.1",
"bs58": "^6.0.0",
"@coinbase/coinbase-sdk": "^0.13.0",
"@coinbase/coinbase-sdk": "^0.14.0",
"csv-parse": "^5.5.6",
"csv-writer": "^1.6.0",
"viem": "^2.21.6"
Expand Down
368 changes: 365 additions & 3 deletions src/client/api.ts

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/coinbase/address/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export class WalletAddress extends Address {
* @param options.assetId - The ID of the Asset to send. For Ether, Coinbase.assets.Eth, Coinbase.assets.Gwei, and Coinbase.assets.Wei supported.
* @param options.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 options.gasless - Whether the Transfer should be gasless. Defaults to false.
* @param options.skipBatching - When true, the Transfer will be submitted immediately. Otherwise, the Transfer will be batched. Defaults to false. Note: requires gasless option to be set to true.
* @returns The transfer object.
* @throws {APIError} if the API request to create a Transfer fails.
* @throws {APIError} if the API request to broadcast a Transfer fails.
Expand All @@ -212,6 +213,7 @@ export class WalletAddress extends Address {
assetId,
destination,
gasless = false,
skipBatching = false,
}: CreateTransferOptions): Promise<Transfer> {
if (!Coinbase.useServerSigner && !this.key) {
throw new Error("Cannot transfer from address without private key loaded");
Expand All @@ -228,12 +230,17 @@ export class WalletAddress extends Address {
);
}

if (skipBatching && !gasless) {
throw new ArgumentError("skipBatching requires gasless to be true");
}

const createTransferRequest = {
amount: asset.toAtomicAmount(normalizedAmount).toString(),
network_id: destinationNetworkId,
asset_id: asset.primaryDenomination(),
destination: destinationAddress,
gasless: gasless,
skip_batching: skipBatching,
};

const response = await Coinbase.apiClients.transfer!.createTransfer(
Expand Down
1 change: 1 addition & 0 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ export type CreateTransferOptions = {
assetId: string;
destination: Destination;
gasless?: boolean;
skipBatching?: boolean;
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/coinbase/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export class Wallet {
* @param options.assetId - The ID of the Asset to send.
* @param options.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 options.gasless - Whether the Transfer should be gasless. Defaults to false.
* @param options.skipBatching - When true, the Transfer will be submitted immediately. Otherwise, the Transfer will be batched. Defaults to false. Note: requires gasless option to be set to true.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should frame this to be something like "to achieve a high throughput, enable batching"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Will add to the docs

* @returns The created Transfer object.
* @throws {APIError} if the API request to create a Transfer fails.
* @throws {APIError} if the API request to broadcast a Transfer fails.
Expand Down
58 changes: 58 additions & 0 deletions src/tests/wallet_address_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,63 @@ describe("WalletAddress", () => {
expect(transfer.getId()).toBe(VALID_TRANSFER_MODEL.transfer_id);
});

it("should default skipBatching to false", async () => {
Coinbase.apiClients.transfer!.createTransfer = mockReturnValue(VALID_TRANSFER_MODEL);
Coinbase.apiClients.transfer!.broadcastTransfer = mockReturnValue({
transaction_hash: "0x6c087c1676e8269dd81e0777244584d0cbfd39b6997b3477242a008fa9349e11",
...VALID_TRANSFER_MODEL,
});

await address.createTransfer({
amount: weiAmount,
assetId: Coinbase.assets.Wei,
destination,
});

expect(Coinbase.apiClients.transfer!.createTransfer).toHaveBeenCalledWith(
address.getWalletId(),
address.getId(),
expect.objectContaining({
skip_batching: false,
}),
);
});

it("should allow skipBatching to be set to true", async () => {
Coinbase.apiClients.transfer!.createTransfer = mockReturnValue(VALID_TRANSFER_MODEL);
Coinbase.apiClients.transfer!.broadcastTransfer = mockReturnValue({
transaction_hash: "0x6c087c1676e8269dd81e0777244584d0cbfd39b6997b3477242a008fa9349e11",
...VALID_TRANSFER_MODEL,
});

await address.createTransfer({
amount: weiAmount,
assetId: Coinbase.assets.Wei,
destination,
gasless: true,
skipBatching: true,
});

expect(Coinbase.apiClients.transfer!.createTransfer).toHaveBeenCalledWith(
address.getWalletId(),
address.getId(),
expect.objectContaining({
skip_batching: true,
}),
);
});

it("should throw an ArgumentError if skipBatching is true but gasless is false", async () => {
await expect(
address.createTransfer({
amount: weiAmount,
assetId: Coinbase.assets.Wei,
destination,
skipBatching: true,
}),
).rejects.toThrow(ArgumentError);
});

it("should successfully construct createTransfer request when using a large number that causes scientific notation", async () => {
Coinbase.apiClients.transfer!.createTransfer = mockReturnValue(VALID_TRANSFER_MODEL);
Coinbase.apiClients.transfer!.broadcastTransfer = mockReturnValue({
Expand Down Expand Up @@ -745,6 +802,7 @@ describe("WalletAddress", () => {
destination: destination.getId(),
gasless: false,
network_id: Coinbase.networks.BaseSepolia,
skip_batching: false,
},
);

Expand Down
75 changes: 75 additions & 0 deletions src/tests/wallet_transfer_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Wallet } from "../coinbase/wallet";
import { WalletAddress } from "../coinbase/address/wallet_address";
import { newAddressModel } from "./utils";
import { Coinbase, Transfer } from "..";
import { FeatureSet, Wallet as WalletModel } from "../client/api";

describe("Wallet Transfer", () => {
let wallet: Wallet;
let walletModel: WalletModel;
let defaultAddress: WalletAddress;
const walletId = "test-wallet-id";
const addressId = "0x123abc...";

beforeEach(() => {
const addressModel = newAddressModel(walletId, addressId);
defaultAddress = new WalletAddress(addressModel);

walletModel = {
id: walletId,
network_id: Coinbase.networks.BaseSepolia,
default_address: addressModel,
feature_set: {} as FeatureSet,
};

wallet = Wallet.init(walletModel, "");

// Mock getDefaultAddress to return our test address
jest.spyOn(wallet, "getDefaultAddress").mockResolvedValue(defaultAddress);

// Mock the createTransfer method on the default address
jest.spyOn(defaultAddress, "createTransfer").mockResolvedValue({} as Transfer);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe("#createTransfer", () => {
it("should pass through skipBatching to defaultAddress.createTransfer", async () => {
const assetId = "eth";

await wallet.createTransfer({
amount: 1,
assetId,
destination: "0x123abc...",
gasless: true,
skipBatching: true,
});

expect(defaultAddress.createTransfer).toHaveBeenCalledWith({
amount: 1,
assetId,
destination: "0x123abc...",
gasless: true,
skipBatching: true,
});

await wallet.createTransfer({
amount: 1,
assetId,
destination: "0x123abc...",
gasless: true,
skipBatching: false,
});

expect(defaultAddress.createTransfer).toHaveBeenCalledWith({
amount: 1,
assetId,
destination: "0x123abc...",
gasless: true,
skipBatching: false,
});
});
});
});
Loading