Skip to content

Commit

Permalink
feat(PSDK-782): allow users to send sponsored transactions immediately (
Browse files Browse the repository at this point in the history
  • Loading branch information
0xRAG authored Jan 14, 2025
1 parent 343e123 commit 69c24c9
Show file tree
Hide file tree
Showing 8 changed files with 523 additions and 3 deletions.
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

## Unreleased

### 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
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.
* @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,
});
});
});
});

0 comments on commit 69c24c9

Please sign in to comment.