Skip to content

Commit

Permalink
code review
Browse files Browse the repository at this point in the history
  • Loading branch information
John-peterson-coinbase committed May 16, 2024
1 parent f17c786 commit d538a1f
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 69 deletions.
19 changes: 17 additions & 2 deletions src/coinbase/coinbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,25 @@ export class Coinbase {
BaseSepolia: "base-sepolia",
};

/**
* The list of supported assets.
*
* @constant
*/
static assetList = {
Eth: "eth",
Wei: "wei",
Gwei: "gwei",
Usdc: "usdc",
Weth: "weth",
};

apiClients: ApiClients = {};

/**
* Represents the number of Wei per Ether.
*
* @constant
*/
static readonly WEI_PER_ETHER: bigint = BigInt("1000000000000000000");

Expand Down Expand Up @@ -75,7 +90,7 @@ export class Coinbase {
* @param filePath - The path to the JSON file containing the API key and private key.
* @param debugging - If true, logs API requests and responses to the console.
* @param basePath - The base path for the API.
* @returns {Coinbase} A new instance of the Coinbase SDK.
* @returns A new instance of the Coinbase SDK.
* @throws {InvalidAPIKeyFormat} If the file does not exist or the configuration values are missing/invalid.
* @throws {InvalidConfiguration} If the configuration is invalid.
* @throws {InvalidAPIKeyFormat} If not able to create JWT token.
Expand Down Expand Up @@ -110,7 +125,7 @@ export class Coinbase {
/**
* Returns User object for the default user.
*
* @returns {User} The default user.
* @returns The default user.
* @throws {APIError} If the request fails.
*/
async getDefaultUser(): Promise<User> {
Expand Down
1 change: 1 addition & 0 deletions src/coinbase/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class InvalidUnsignedPayload extends Error {

/**
* Initializes a new InvalidUnsignedPayload instance.
*
* @param message - The error message.
*/
constructor(message: string = InvalidUnsignedPayload.DEFAULT_MESSAGE) {
Expand Down
11 changes: 7 additions & 4 deletions src/coinbase/tests/transfer_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers } from "ethers";
import { Transfer as TransferModel, TransferStatusEnum } from "../../client/api";
import { ApiClients, TransferStatus } from "../types";
import { Transfer } from "../transfer";
import { TransferAPIClient, TransferStatus } from "../types";
import { Transfer, TransferClients } from "../transfer";
import { Coinbase } from "../coinbase";

const fromKey = ethers.Wallet.createRandom();
Expand Down Expand Up @@ -41,7 +41,7 @@ mockProvider.getTransactionReceipt = jest.fn();

describe("Transfer Class", () => {
let transferModel: TransferModel;
let mockApiClients: ApiClients;
let mockApiClients: TransferClients;
let transfer: Transfer;

beforeEach(() => {
Expand All @@ -57,7 +57,10 @@ describe("Transfer Class", () => {
status: TransferStatusEnum.Pending,
} as TransferModel;

mockApiClients = { baseSepoliaProvider: mockProvider } as ApiClients;
mockApiClients = {
transfer: {} as TransferAPIClient,
baseSepoliaProvider: mockProvider,
} as TransferClients;

transfer = new Transfer(transferModel, mockApiClients);
});
Expand Down
149 changes: 86 additions & 63 deletions src/coinbase/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { ApiClients, TransferStatus } from "./types";
import { TransferAPIClient, TransferStatus } from "./types";
import { Coinbase } from "./coinbase";
import { Transfer as TransferModel } from "../client/api";
import { ethers } from "ethers";
import { InternalError, InvalidUnsignedPayload } from "./errors";

/**
* A representation of a transfer, which moves an amount of an asset from
* a user-controlled wallet to another address. The fee is assumed to be paid
* in the native asset of the network.
* The Transfer API client types.
*/
export type TransferClients = {
transfer: TransferAPIClient;
baseSepoliaProvider: ethers.Provider;
};

/**
* A representation of a Transfer, which moves an Amount of an Asset from
* a user-controlled Wallet to another Address. The fee is assumed to be paid
* in the native Asset of the Network.
*/
export class Transfer {
private model: TransferModel;
private client: ApiClients;
private client: TransferClients;
private transaction?: ethers.Transaction;

/**
* Initializes a new Transfer instance.
* @param {TransferModel} transferModel - The transfer model.
* @param {ApiClients} client - The API clients.
*
* @param transferModel - The Transfer model.
* @param client - The API clients.
*/
constructor(transferModel: TransferModel, client: ApiClients) {
constructor(transferModel: TransferModel, client: TransferClients) {
if (!transferModel) {
throw new InternalError("Transfer model cannot be empty");
}
Expand All @@ -32,94 +41,105 @@ export class Transfer {
}

/**
* Returns the ID of the transfer.
* @returns {string} The transfer ID.
* Returns the ID of the Transfer.
*
* @returns The Transfer ID.
*/
public getId(): string {
return this.model.transfer_id;
}

/**
* Returns the network ID of the transfer.
* @returns {string} The network ID.
* Returns the Network ID of the Transfer.
*
* @returns The Network ID.
*/
public getNetworkId(): string {
return this.model.network_id;
}

/**
* Returns the wallet ID of the transfer.
* @returns {string} The wallet ID.
* Returns the Wallet ID of the Transfer.
*
* @returns The Wallet ID.
*/
public getWalletId(): string {
return this.model.wallet_id;
}

/**
* Returns the from address ID of the transfer.
* @returns {string} The from address ID.
* Returns the From Address ID of the Transfer.
*
* @returns The From Address ID.
*/
public getFromAddressId(): string {
return this.model.address_id;
}

/**
* Returns the destination address ID of the transfer.
* @returns {string} The destination address ID.
* Returns the Destination Address ID of the Transfer.
*
* @returns The Destination Address ID.
*/
public getDestinationAddressId(): string {
return this.model.destination;
}

/**
* Returns the asset ID of the transfer.
* @returns {string} The asset ID.
* Returns the Asset ID of the Transfer.
*
* @returns The Asset ID.
*/
public getAssetId(): string {
return this.model.asset_id;
}

/**
* Returns the amount of the transfer.
* @returns {string} The amount of the asset.
* Returns the Amount of the Transfer.
*
* @returns The Amount of the Asset.
*/
public getAmount(): bigint {
const amount = BigInt(this.model.amount);

if (this.getAssetId() === "eth") {
if (this.getAssetId() === Coinbase.assetList.Eth) {
return amount / BigInt(Coinbase.WEI_PER_ETHER);
}
return BigInt(this.model.amount);
}

/**
* Returns the unsigned payload of the transfer.
* @returns {string} The unsigned payload as a hex string.
* Returns the Unsigned Payload of the Transfer.
*
* @returns The Unsigned Payload as a Hex string.
*/
public getUnsignedPayload(): string {
return this.model.unsigned_payload;
}

/**
* Returns the signed payload of the transfer.
* @returns {string | undefined} The signed payload as a hex string, or undefined if not yet available.
* Returns the Signed Payload of the Transfer.
*
* @returns The Signed Payload as a Hex string, or undefined if not yet available.
*/
public getSignedPayload(): string | undefined {
return this.model.signed_payload;
}

/**
* Returns the transaction hash of the transfer.
* @returns {string | undefined} The transaction hash as a hex string, or undefined if not yet available.
* Returns the Transaction Hash of the Transfer.
*
* @returns The Transaction Hash as a Hex string, or undefined if not yet available.
*/
public getTransactionHash(): string | undefined {
return this.model.transaction_hash;
}

/**
* Returns the transaction of the transfer.
* @returns {ethers.Transaction} The ethers.js Transaction object.
* @throws (InvalidUnsignedPayload) If the unsigned payload is invalid.
* Returns the Transaction of the Transfer.
*
* @returns The ethers.js Transaction object.
* @throws (InvalidUnsignedPayload) If the Unsigned Payload is invalid.
*/
public getTransaction(): ethers.Transaction {
if (this.transaction) return this.transaction;
Expand All @@ -133,62 +153,63 @@ export class Transfer {
throw new InvalidUnsignedPayload("Unable to parse unsigned payload");
}

const rawPayloadBytes = new Uint8Array(rawPayload);

const decoder = new TextDecoder();

let parsedPayload;
try {
const rawPayloadBytes = new Uint8Array(rawPayload);
const decoder = new TextDecoder();
parsedPayload = JSON.parse(decoder.decode(rawPayloadBytes));
} catch (error) {
throw new InvalidUnsignedPayload("Unable to decode unsigned payload JSON");
}

transaction.chainId = BigInt(parsedPayload["chainId"]);
transaction.nonce = BigInt(parsedPayload["nonce"]);
transaction.maxPriorityFeePerGas = BigInt(parsedPayload["maxPriorityFeePerGas"]);
transaction.maxFeePerGas = BigInt(parsedPayload["maxFeePerGas"]);
transaction.gasLimit = BigInt(parsedPayload["gas"]);
transaction.to = parsedPayload["to"];
transaction.value = BigInt(parsedPayload["value"]);
transaction.data = parsedPayload["input"];
transaction.chainId = BigInt(parsedPayload.chainId);
transaction.nonce = BigInt(parsedPayload.nonce);
transaction.maxPriorityFeePerGas = BigInt(parsedPayload.maxPriorityFeePerGas);
transaction.maxFeePerGas = BigInt(parsedPayload.maxFeePerGas);
transaction.gasLimit = BigInt(parsedPayload.gas);
transaction.to = parsedPayload.to;
transaction.value = BigInt(parsedPayload.value);
transaction.data = parsedPayload.input;

this.transaction = transaction;
return transaction;
}

/**
* Sets the signed transaction of the transfer.
* @param {ethers.Transaction} transaction - The signed transaction.
* Sets the Signed Transaction of the Transfer.
*
* @param transaction - The Signed Transaction.
*/
public setSignedTransaction(transaction: ethers.Transaction): void {
this.transaction = transaction;
}

/**
* Returns the status of the transfer.
* @returns {Promise<TransferStatus>} The status of the transfer.
* Returns the Status of the Transfer.
*
* @returns The Status of the Transfer.
*/
public async getStatus(): Promise<TransferStatus> {
const transactionHash = this.getTransactionHash();
if (!transactionHash) return TransferStatus.PENDING;

const onchainTransansaction =
const onchainTransaction =
await this.client.baseSepoliaProvider!.getTransaction(transactionHash);
if (!onchainTransansaction) return TransferStatus.PENDING;
if (!onchainTransansaction.blockHash) return TransferStatus.BROADCAST;
if (!onchainTransaction) return TransferStatus.PENDING;
if (!onchainTransaction.blockHash) return TransferStatus.BROADCAST;

const transactionReceipt =
await this.client.baseSepoliaProvider!.getTransactionReceipt(transactionHash);
return transactionReceipt?.status ? TransferStatus.COMPLETE : TransferStatus.FAILED;
}

/**
* Waits until the transfer is completed or failed by polling the network at the given interval.
* Raises an error if the transfer takes longer than the given timeout.
* @param {number} intervalSeconds - The interval at which to poll the network, in seconds.
* @param {number} timeoutSeconds - The maximum amount of time to wait for the transfer to complete, in seconds.
* @returns {Promise<Transfer>} The completed Transfer object.
* Waits until the Transfer is completed or failed by polling the Network at the given interval.
*
* @param intervalSeconds - The interval at which to poll the Network, in seconds.
* @param timeoutSeconds - The maximum amount of time to wait for the Transfer to complete, in seconds.
* @returns The completed Transfer object.
* @throws {Error} if the Transfer takes longer than the given timeout.
*/
public async wait(intervalSeconds = 0.2, timeoutSeconds = 10): Promise<Transfer> {
const startTime = Date.now();
Expand All @@ -203,24 +224,26 @@ export class Transfer {
}

/**
* Returns the link to the transaction on the blockchain explorer.
* @returns {string} The link to the transaction on the blockchain explorer.
* Returns the link to the Transaction on the blockchain explorer.
*
* @returns The link to the Transaction on the blockchain explorer.
*/
public getTransactionLink(): string {
return `https://sepolia.basescan.org/tx/${this.getTransactionHash()}`;
}

/**
* Returns a string representation of the Transfer.
* @returns {Promise<string>} a string representation of the Transfer.
*
* @returns The string representation of the Transfer.
*/
public async toString(): Promise<string> {
const status = await this.getStatus();
return (
`Coinbase::Transfer{transfer_id: '${this.getId()}', network_id: '${this.getNetworkId()}', ` +
`from_address_id: '${this.getFromAddressId()}', destination_address_id: '${this.getDestinationAddressId()}', ` +
`asset_id: '${this.getAssetId()}', amount: '${this.getAmount()}', transaction_hash: '${this.getTransactionHash()}', ` +
`transaction_link: '${this.getTransactionLink()}', status: '${status}'}`
`Transfer{transferId: '${this.getId()}', networkId: '${this.getNetworkId()}', ` +
`fromAddressId: '${this.getFromAddressId()}', destinationAddressId: '${this.getDestinationAddressId()}', ` +
`assetId: '${this.getAssetId()}', amount: '${this.getAmount()}', transactionHash: '${this.getTransactionHash()}', ` +
`transactionLink: '${this.getTransactionLink()}', status: '${status}'}`
);
}
}

0 comments on commit d538a1f

Please sign in to comment.