Skip to content

Commit

Permalink
Implementing trading functionality in Asset Class
Browse files Browse the repository at this point in the history
  • Loading branch information
erdimaden committed Jun 8, 2024
1 parent b5abbfb commit df546ce
Show file tree
Hide file tree
Showing 10 changed files with 598 additions and 7 deletions.
115 changes: 115 additions & 0 deletions src/coinbase/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Amount, Destination, TransferStatus } from "./types";
import { Transfer } from "./transfer";
import { delay, destinationToAddressHexString } from "./utils";
import { ATOMIC_UNITS_PER_USDC, WEI_PER_ETHER, WEI_PER_GWEI } from "./constants";
import { Asset } from "./asset";
import { Trade } from "./trade";

/**
* A representation of a blockchain address, which is a user-controlled account on a network.
Expand Down Expand Up @@ -263,6 +265,119 @@ export class Address {
throw new Error("Transfer timed out");
}

/**
* 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. For Ether, eth, gwei, and wei are supported.
* @param toAssetId - The ID of the Asset to trade to. For Ether, eth, gwei, and wei are supported.
* @returns The Trade object.
* @throws {Error} Will throw an error if the private key is not loaded, or if the asset IDs are unsupported, or if there are insufficient funds.
*/
public async trade(amount: Amount, fromAssetId: string, toAssetId: string): Promise<Trade> {
await this.validateCanTrade(amount, fromAssetId, toAssetId);
const trade = await this.createTrade(amount, fromAssetId, toAssetId);
// 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.broadcastTrade(trade, signed_payload, approveTransactionSignedPayload);
}

/**
* Creates a trade model for the specified amount and assets.
*
* @param amount - The amount of the Asset to send.
* @param fromAssetId - The ID of the Asset to trade from. For Ether, eth, gwei, and wei are supported.
* @param toAssetId - The ID of the Asset to trade to. For Ether, eth, gwei, and wei are supported.
* @returns A promise that resolves to a Trade object representing the new trade.
*/
private async createTrade(
amount: Amount,
fromAssetId: string,
toAssetId: string,
): Promise<Trade> {
const tradeRequestPayload = {
amount: Asset.toAtomicAmount(new Decimal(amount.toString()), fromAssetId).toString(),
from_asset_id: Asset.primaryDenomination(fromAssetId),
to_asset_id: Asset.primaryDenomination(toAssetId),
};
const tradeModel = await Coinbase.apiClients.trade!.createTrade(
this.getWalletId(),
this.getId(),
tradeRequestPayload,
);
return new Trade(tradeModel?.data);
}

/**
* Broadcasts a trade using the provided signed payloads.
*
* @param trade - The Trade object representing the trade.
* @param signedPayload - The signed payload of the trade.
* @param approveTransactionPayload - The signed payload of the approval transaction, if any.
* @returns A promise that resolves to a Trade object representing the broadcasted trade.
*/
private async broadcastTrade(
trade: Trade,
signedPayload: string,
approveTransactionPayload?: string,
): Promise<Trade> {
const broadcastTradeRequest = {
signed_payload: signedPayload,
approve_transaction_signed_payload: approveTransactionPayload
? approveTransactionPayload
: undefined,
};

const response = await Coinbase.apiClients.trade!.broadcastTrade(
this.getWalletId(),
this.getId(),
trade.getId(),
broadcastTradeRequest,
);

return new Trade(response.data);
}

/**
* Checks if trading is possible and raises an error if not.
*
* @param amount - The amount of the Asset to send.
* @param fromAssetId - The ID of the Asset to trade from. For Ether, eth, gwei, and wei are supported.
* @param toAssetId - The ID of the Asset to trade to. For Ether, eth, gwei, and wei are supported.
* @throws {Error} Will throw an 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, toAssetId: string) {
if (!this.canSign()) {
throw new Error("Cannot trade from address without private key loaded");
}
if (!Asset.isSupported(fromAssetId)) {
throw new Error(`Unsupported from asset: ${fromAssetId}`);
}
if (!Asset.isSupported(toAssetId)) {
throw new Error(`Unsupported to asset: ${toAssetId}`);
}
const currentBalance = await this.getBalance(fromAssetId);
amount = new Decimal(amount.toString());
if (currentBalance.lessThan(amount)) {
throw new Error(
`Insufficient funds: ${amount} requested, but only ${currentBalance} available`,
);
}
}

/**
* Returns whether the Address has a private key backing it to sign transactions.
*
* @returns Whether the Address has a private key backing it to sign transactions.
*/
public canSign(): boolean {
return !!this.key;
}

/**
* Returns a string representation of the address.
*
Expand Down
54 changes: 52 additions & 2 deletions src/coinbase/asset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Decimal from "decimal.js";
import { ATOMIC_UNITS_PER_USDC, WEI_PER_ETHER, WEI_PER_GWEI } from "./constants";
import {
ATOMIC_UNITS_PER_USDC,
SUPPORTED_ASSET_IDS,
WEI_PER_ETHER,
WEI_PER_GWEI,
} from "./constants";
import { Coinbase } from "./coinbase";

/** A representation of an Asset. */
export class Asset {
Expand All @@ -8,7 +14,7 @@ export class Asset {
* to whole units of the specified asset ID.
*
* @param {Decimal} atomicAmount - The amount in atomic units.
* @param {string} assetId - The assset ID.
* @param {string} assetId - The asset ID.
* @returns The amount in whole units of the asset with the specified ID.
*/
static fromAtomicAmount(atomicAmount: Decimal, assetId: string): Decimal {
Expand All @@ -25,4 +31,48 @@ export class Asset {
return atomicAmount;
}
}

/**
* Converts the amount of the Asset to the atomic units of the primary denomination of the Asset.
*
* @param amount - The amount to normalize.
* @param assetId - The ID of the Asset being transferred.
* @returns The normalized amount in atomic units.
*/
static toAtomicAmount(amount: Decimal, assetId: string): Decimal {
switch (assetId) {
case "eth":
return amount.mul(WEI_PER_ETHER);
case "gwei":
return amount.mul(WEI_PER_GWEI);
case "usdc":
return amount.mul(ATOMIC_UNITS_PER_USDC);
case "weth":
return amount.mul(WEI_PER_ETHER);
default:
return amount;
}
}

/**
* Returns the primary denomination for the provided Asset ID.
*
* @param assetId - The Asset ID.
* @returns The primary denomination for the Asset ID.
*/
public static primaryDenomination(assetId: string): string {
return [Coinbase.assets.Gwei, Coinbase.assets.Wei].includes(assetId)
? Coinbase.assets.Eth
: assetId;
}

/**
* Returns whether the provided asset ID is supported.
*
* @param assetId - The Asset ID.
* @returns Whether the Asset ID is supported.
*/
public static isSupported(assetId): boolean {
return SUPPORTED_ASSET_IDS.has(assetId);
}
}
1 change: 1 addition & 0 deletions src/coinbase/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const WEI_PER_ETHER = new Decimal("1000000000000000000");
export const WEI_PER_GWEI = new Decimal("1000000000");
export const GWEI_PER_ETHER = new Decimal("1000000000");
export const ATOMIC_UNITS_PER_USDC = new Decimal("1000000");
export const SUPPORTED_ASSET_IDS = new Set(["eth", "gwei", "wei", "usdc", "weth"]);
Loading

0 comments on commit df546ce

Please sign in to comment.