Skip to content

Commit

Permalink
[PSDK-348] support trades with server signer (#119)
Browse files Browse the repository at this point in the history
* [PSDK-348] Trade Verb Support w/ MPC Server-Signer

* jazz review feedback
  • Loading branch information
John-peterson-coinbase authored Jul 30, 2024
1 parent 33f7f40 commit ff0de34
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 83 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Coinbase Node.js SDK Changelog

## Unreleased

### Added

- Support for trade with MPC Server-Signer
- `CreateTradeOptions` type

## [0.0.12] - 2024-07-24

### Changed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ console.log(`Wallet successfully created: ${mainnetWallet}`);
// Fund your Wallet's default Address with ETH from an external source.

// Trade 0.00001 ETH to USDC
let trade = await wallet.createTrade(0.00001, Coinbase.assets.Eth, Coinbase.assets.Usdc);
let trade = await wallet.createTrade({ amount: 0.00001, fromAssetId: Coinbase.assets.Eth, toAssetId: Coinbase.assets.Usdc });

console.log(`Second trade successfully completed: ${trade}`);
```
Expand Down Expand Up @@ -222,4 +222,4 @@ await userWallet.loadSeed(seedFilePath);
[build-size]: https://badgen.net/bundlephobia/minzip/@coinbase/coinbase-sdk
[build-size-url]: https://bundlephobia.com/result?p=@coinbase/coinbase-sdk
[npmtrends-url]: https://www.npmtrends.com/@coinbase/coinbase-sdk
[npm-downloads]: https://img.shields.io/npm/dw/@coinbase/coinbase-sdk
[npm-downloads]: https://img.shields.io/npm/dw/@coinbase/coinbase-sdk
2 changes: 1 addition & 1 deletion quickstart-template/trade_assets.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Coinbase } from "@coinbase/coinbase-sdk";

let coinbase = Coinbase.configureFromJson({ filePath: '~/Downloads/cdp_api_key.json' });
let coinbase = Coinbase.configureFromJson({ filePath: "~/Downloads/cdp_api_key.json" });
let user = await coinbase.getDefaultUser();

// Create a Wallet on base-mainnet to trade assets with.
Expand Down
72 changes: 53 additions & 19 deletions src/coinbase/address/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { Coinbase } from "../coinbase";
import { ArgumentError, InternalError } from "../errors";
import { Trade } from "../trade";
import { Transfer } from "../transfer";
import { Amount, CreateTransferOptions, Destination, TransferStatus } from "../types";
import {
Amount,
CreateTransferOptions,
CreateTradeOptions,
Destination,
TransferStatus,
TransactionStatus,
} from "../types";
import { delay } from "../utils";
import { Wallet as WalletClass } from "../wallet";

Expand Down Expand Up @@ -67,9 +74,9 @@ export class WalletAddress extends Address {
}

/**
* Returns all the transfers associated with the address.
* Returns all the trades associated with the address.
*
* @returns The list of transfers.
* @returns The list of trades.
*/
public async listTrades(): Promise<Trade[]> {
const trades: Trade[] = [];
Expand All @@ -84,8 +91,8 @@ export class WalletAddress extends Address {
page?.length ? page : undefined,
);

response.data.data.forEach(transferModel => {
trades.push(new Trade(transferModel));
response.data.data.forEach(tradeModel => {
trades.push(new Trade(tradeModel));
});

if (response.data.has_more) {
Expand Down Expand Up @@ -244,25 +251,52 @@ export class WalletAddress extends Address {
/**
* Trades the given amount of the given Asset for another Asset. Only same-network Trades are supported.
*
* @param amount - The amount of the Asset to send.
* @param fromAssetId - The ID of the Asset to trade from.
* @param toAssetId - The ID of the Asset to trade to.
* @param options = The options to create the Trade.
* @param options.amount - The amount of the From Asset to send.
* @param options.fromAssetId - The ID of the Asset to trade from.
* @param options.toAssetId - The ID of the Asset to trade to.
* @param options.timeoutSeconds - The maximum amount of time to wait for the Trade to complete, in seconds.
* @param options.intervalSeconds - The interval at which to poll the Network for Trade status, in seconds.
* @returns The Trade object.
* @throws {Error} If the private key is not loaded, or if the asset IDs are unsupported, or if there are insufficient funds.
* @throws {APIError} if the API request to create or broadcast a Trade fails.
* @throws {Error} if the Trade times out.
*/
public async createTrade(amount: Amount, fromAssetId: string, toAssetId: string): Promise<Trade> {
public async createTrade({
amount,
fromAssetId,
toAssetId,
timeoutSeconds = 10,
intervalSeconds = 0.2,
}: CreateTradeOptions): Promise<Trade> {
const fromAsset = await Asset.fetch(this.getNetworkId(), fromAssetId);
const toAsset = await Asset.fetch(this.getNetworkId(), toAssetId);

await this.validateCanTrade(amount, fromAssetId);
const trade = await this.createTradeRequest(amount, fromAsset, toAsset);
// NOTE: Trading does not yet support server signers at this point.
const signed_payload = await trade.getTransaction().sign(this.key!);
const approveTransactionSignedPayload = trade.getApproveTransaction()
? await trade.getApproveTransaction()!.sign(this.key!)
: undefined;

return this.broadcastTradeRequest(trade, signed_payload, approveTransactionSignedPayload);
let trade = await this.createTradeRequest(amount, fromAsset, toAsset);

if (!Coinbase.useServerSigner) {
const signed_payload = await trade.getTransaction().sign(this.key!);
const approveTransactionSignedPayload = trade.getApproveTransaction()
? await trade.getApproveTransaction()!.sign(this.key!)
: undefined;

trade = await this.broadcastTradeRequest(
trade,
signed_payload,
approveTransactionSignedPayload,
);
}

const startTime = Date.now();
while (Date.now() - startTime < timeoutSeconds * 1000) {
await trade.reload();
const status = trade.getStatus();
if (status === TransactionStatus.COMPLETE || status === TransactionStatus.FAILED) {
return trade;
}
await delay(intervalSeconds);
}
throw new Error("Trade timed out");
}

/**
Expand Down Expand Up @@ -329,7 +363,7 @@ export class WalletAddress extends Address {
* @throws {Error} If the private key is not loaded, or if the asset IDs are unsupported, or if there are insufficient funds.
*/
private async validateCanTrade(amount: Amount, fromAssetId: string) {
if (!this.canSign()) {
if (!Coinbase.useServerSigner && !this.key) {
throw new Error("Cannot trade from address without private key loaded");
}
const currentBalance = await this.getBalance(fromAssetId);
Expand Down
11 changes: 11 additions & 0 deletions src/coinbase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,3 +718,14 @@ export type CreateTransferOptions = {
timeoutSeconds?: number;
intervalSeconds?: number;
};

/**
* Options for creating a Trade.
*/
export type CreateTradeOptions = {
amount: Amount;
fromAssetId: string;
toAssetId: string;
timeoutSeconds?: number;
intervalSeconds?: number;
};
27 changes: 21 additions & 6 deletions src/coinbase/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { FaucetTransaction } from "./faucet_transaction";
import { Trade } from "./trade";
import { Transfer } from "./transfer";
import {
Amount,
CreateTransferOptions,
CreateTradeOptions,
SeedData,
ServerSignerStatus,
WalletCreateOptions,
Expand Down Expand Up @@ -275,18 +275,33 @@ export class Wallet {
/**
* Trades the given amount of the given Asset for another Asset. Currently only the default address is used to source the Trade
*
* @param amount - The amount of the Asset to send.
* @param fromAssetId - The ID of the Asset to trade from.
* @param toAssetId - The ID of the Asset to trade to.
* @param options - The options to create the Trade.
* @param options.amount - The amount of the Asset to send.
* @param options.fromAssetId - The ID of the Asset to trade from.
* @param options.toAssetId - The ID of the Asset to trade to.
* @param options.timeoutSeconds - The maximum amount of time to wait for the Trade to complete, in seconds.
* @param options.intervalSeconds - The interval at which to poll the Network for Trade status, in seconds.
* @throws {InternalError} If the default address is not found.
* @throws {Error} If the private key is not loaded, or if the asset IDs are unsupported, or if there are insufficient funds.
* @returns The Trade object.
*/
public async createTrade(amount: Amount, fromAssetId: string, toAssetId: string): Promise<Trade> {
public async createTrade({
amount,
fromAssetId,
toAssetId,
timeoutSeconds = 10,
intervalSeconds = 0.2,
}: CreateTradeOptions): Promise<Trade> {
if (!this.getDefaultAddress()) {
throw new InternalError("Default address not found");
}
return await this.getDefaultAddress()!.createTrade(amount, fromAssetId, toAssetId);
return await this.getDefaultAddress()!.createTrade({
amount: amount,
fromAssetId: fromAssetId,
toAssetId: toAssetId,
timeoutSeconds,
intervalSeconds,
});
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/examples/trade_assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ async function tradeAssets() {

// Fund the wallet's default address with ETH from an external source.
// Trade 0.00001 ETH to USDC
const trade = await wallet.createTrade(0.00001, Coinbase.assets.Eth, Coinbase.assets.Usdc);
const trade = await wallet.createTrade({
amount: 0.00001,
fromAssetId: Coinbase.assets.Eth,
toAssetId: Coinbase.assets.Usdc,
});
console.log(`Trade successfully completed: `, trade.toString());
}

Expand Down
Loading

0 comments on commit ff0de34

Please sign in to comment.