Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: coinbase/coinbase-sdk-nodejs
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: c8b5e85c3a7d568e06e735a40336fb6a60c329c3
Choose a base ref
..
head repository: coinbase/coinbase-sdk-nodejs
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: b50a4395f267ca817afbcbe4fc3ec091f82b9bbf
Choose a head ref
2 changes: 2 additions & 0 deletions src/coinbase/errors.ts
Original file line number Diff line number Diff line change
@@ -106,6 +106,8 @@ export class AlreadySignedError extends Error {

/**
* Initializes a new AlreadySignedError instance.
*
* @param message - The error message.
*/
constructor(message: string = AlreadySignedError.DEFAULT_MESSAGE) {
super(message);
6 changes: 2 additions & 4 deletions src/coinbase/sponsored_send.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import { SponsoredSendStatus } from "./types";
*/
export class SponsoredSend {
private model: SponsoredSendModel;
private signed: boolean | undefined;

/**
* Sponsored Sends should be constructed via higher level abstractions like Transfer.
@@ -51,7 +50,6 @@ export class SponsoredSend {
ethers.toBeArray;
const signature = key.signingKey.sign(ethers.getBytes(this.getTypedDataHash())).serialized;
this.model.signature = signature;
this.signed = true;
// Removes the '0x' prefix as required by the API.
return signature.slice(2);
}
@@ -61,8 +59,8 @@ export class SponsoredSend {
*
* @returns if the Sponsored Send has been signed.
*/
isSigned(): boolean | undefined {
return this.signed;
isSigned(): boolean {
return this.getSignature() ? true : false;
}

/**
6 changes: 2 additions & 4 deletions src/coinbase/transaction.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ import { parseUnsignedPayload } from "./utils";
export class Transaction {
private model: TransactionModel;
private raw?: ethers.Transaction;
private signed: boolean | undefined;

/**
* Transactions should be constructed via higher level abstractions like Trade or Transfer.
@@ -136,7 +135,6 @@ export class Transaction {
async sign(key: ethers.Wallet) {
const signedPayload = await key!.signTransaction(this.rawTransaction());
this.model.signed_payload = signedPayload;
this.signed = true;
// Removes the '0x' prefix as required by the API.
return signedPayload.slice(2);
}
@@ -155,8 +153,8 @@ export class Transaction {
*
* @returns if the transaction has been signed.
*/
isSigned(): boolean | undefined {
return this.signed;
isSigned(): boolean {
return this.getSignature() ? true : false;
}

/**
3 changes: 1 addition & 2 deletions src/coinbase/transfer.ts
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@ import { InternalError } from "./errors";
*/
export class Transfer {
private model: TransferModel;
private transaction?: ethers.Transaction;

/**
* Private constructor to prevent direct instantiation outside of the factory methods.
@@ -211,7 +210,7 @@ export class Transfer {
*/
public async broadcast(): Promise<Transfer> {
if (!this.getSendTransactionDelegate()?.isSigned())
throw new Error("Cannot broadcast transfer without signature"); // TODO: make a derived class error for this case.
throw new Error("Cannot broadcast unsigned Transfer");

const broadcastTransferRequest = {
signed_payload: this.getSendTransactionDelegate()!.getSignature()!.slice(2),
33 changes: 23 additions & 10 deletions src/tests/error_test.ts
Original file line number Diff line number Diff line change
@@ -4,66 +4,79 @@ import {
InvalidAPIKeyFormat,
InvalidConfiguration,
InvalidUnsignedPayload,
AlreadySignedError,
} from "../coinbase/errors";

describe("Error Classes", () => {
test("InvalidAPIKeyFormat should have the correct message and name", () => {
it("InvalidAPIKeyFormat should have the correct message and name", () => {
const error = new InvalidAPIKeyFormat();
expect(error.message).toBe(InvalidAPIKeyFormat.DEFAULT_MESSAGE);
expect(error.name).toBe("InvalidAPIKeyFormat");
});

test("InvalidAPIKeyFormat should accept a custom message", () => {
it("InvalidAPIKeyFormat should accept a custom message", () => {
const customMessage = "Custom invalid API key format message";
const error = new InvalidAPIKeyFormat(customMessage);
expect(error.message).toBe(customMessage);
});

test("ArgumentError should have the correct message and name", () => {
it("ArgumentError should have the correct message and name", () => {
const error = new ArgumentError();
expect(error.message).toBe(ArgumentError.DEFAULT_MESSAGE);
expect(error.name).toBe("ArgumentError");
});

test("ArgumentError should accept a custom message", () => {
it("ArgumentError should accept a custom message", () => {
const customMessage = "Custom argument error message";
const error = new ArgumentError(customMessage);
expect(error.message).toBe(customMessage);
});

test("InternalError should have the correct message and name", () => {
it("InternalError should have the correct message and name", () => {
const error = new InternalError();
expect(error.message).toBe(InternalError.DEFAULT_MESSAGE);
expect(error.name).toBe("InternalError");
});

test("InternalError should accept a custom message", () => {
it("InternalError should accept a custom message", () => {
const customMessage = "Custom internal error message";
const error = new InternalError(customMessage);
expect(error.message).toBe(customMessage);
});

test("InvalidConfiguration should have the correct message and name", () => {
it("InvalidConfiguration should have the correct message and name", () => {
const error = new InvalidConfiguration();
expect(error.message).toBe(InvalidConfiguration.DEFAULT_MESSAGE);
expect(error.name).toBe("InvalidConfiguration");
});

test("InvalidConfiguration should accept a custom message", () => {
it("InvalidConfiguration should accept a custom message", () => {
const customMessage = "Custom invalid configuration message";
const error = new InvalidConfiguration(customMessage);
expect(error.message).toBe(customMessage);
});

test("InvalidUnsignedPayload should have the correct message and name", () => {
it("InvalidUnsignedPayload should have the correct message and name", () => {
const error = new InvalidUnsignedPayload();
expect(error.message).toBe(InvalidUnsignedPayload.DEFAULT_MESSAGE);
expect(error.name).toBe("InvalidUnsignedPayload");
});

test("InvalidUnsignedPayload should accept a custom message", () => {
it("InvalidUnsignedPayload should accept a custom message", () => {
const customMessage = "Custom invalid unsigned payload message";
const error = new InvalidUnsignedPayload(customMessage);
expect(error.message).toBe(customMessage);
});

it("AlreadySignedError should have the correct message and name", () => {
const error = new AlreadySignedError();
expect(error.message).toBe(AlreadySignedError.DEFAULT_MESSAGE);
expect(error.name).toBe("AlreadySIgnedError");
});

it("AlreadySignedError should accept a custom message", () => {
const customMessage = "Custom already signed error message";
const error = new AlreadySignedError(customMessage);
expect(error.message).toBe(customMessage);
});
});
196 changes: 196 additions & 0 deletions src/tests/sponsored_send_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { ethers } from "ethers";
import { SponsoredSend as SponsoredSendModel } from "../client/api";
import { SponsoredSend } from "./../coinbase/sponsored_send";
import { SponsoredSendStatus } from "../coinbase/types";

describe("SponsoredSend", () => {
let fromKey;
let toAddressId;
let rawTypedData;
let typedDataHash;
let signature;
let transactionHash;
let transactionLink;
let model;
let signedModel;
let completedModel;
let sponsoredSend;

beforeEach(() => {
fromKey = ethers.Wallet.createRandom();
toAddressId = "0x4D9E4F3f4D1A8B5F4f7b1F5b5C7b8d6b2B3b1b0b";
typedDataHash = "0x7523946e17c0b8090ee18c84d6f9a8d63bab4d579a6507f0998dde0791891823";
signature = "0x7523946e17c0b8090ee18c84d6f9a8d63bab4d579a6507f0998dde0791891823";
transactionHash = "0xdea671372a8fff080950d09ad5994145a661c8e95a9216ef34772a19191b5690";
transactionLink = `https://sepolia.basescan.org/tx/${transactionHash}`;

model = {
status: "pending",
to_address_id: toAddressId,
typed_data_hash: typedDataHash,
} as SponsoredSendModel;

signedModel = {
status: "signed",
to_address_id: toAddressId,
typed_data_hash: typedDataHash,
signature: signature,
} as SponsoredSendModel;

completedModel = {
status: "complete",
to_address_id: toAddressId,
typed_data_hash: typedDataHash,
signature: signature,
transaction_hash: transactionHash,
transaction_link: transactionLink,
} as SponsoredSendModel;

sponsoredSend = new SponsoredSend(model);
});

describe("constructor", () => {
it("initializes a new SponsoredSend", () => {
expect(sponsoredSend).toBeInstanceOf(SponsoredSend);
});

it("should raise an error when initialized with a model of a different type", () => {
expect(() => new SponsoredSend(null!)).toThrow("Invalid model type");
});
});

describe("#getTypedDataHash", () => {
it("returns the typed data hash", () => {
expect(sponsoredSend.getTypedDataHash()).toEqual(typedDataHash);
});
});

describe("#getSignature", () => {
it("should return undefined when the SponsoredSend has not been signed", () => {
expect(sponsoredSend.getSignature()).toBeUndefined();
});

it("should return the signature when the SponsoredSend has been signed", () => {
const sponsoredSend = new SponsoredSend(signedModel);
expect(sponsoredSend.getSignature()).toEqual(signature);
});
});

describe("#getTransactionHash", () => {
it("should return undefined when the SponsoredSend has not been broadcast on chain", () => {
expect(sponsoredSend.getTransactionHash()).toBeUndefined();
});

it("should return the transaction hash when the SponsoredSend has been broadcast on chain", () => {
const sponsoredSend = new SponsoredSend(completedModel);
expect(sponsoredSend.getTransactionHash()).toEqual(transactionHash);
});
});

describe("#getTransactionLink", () => {
it("should return the transaction link when the transaction hash is available", () => {
const sponsoredSend = new SponsoredSend(completedModel);
expect(sponsoredSend.getTransactionLink()).toEqual(
`https://sepolia.basescan.org/tx/${transactionHash}`,
);
});
});

describe("#sign", () => {
let signature: string;

beforeEach(async () => {
signature = await sponsoredSend.sign(fromKey);
});

it("should return a string when the SponsoredSend is signed", async () => {
expect(typeof signature).toBe("string");
});

it("signs the raw typed data hash", async () => {
expect(signature).not.toBeNull();
});

it("returns a hex representation of the signed typed data hash", async () => {
expect(signature).not.toBeNull();
expect(signature.length).toBeGreaterThan(0);
});

it("sets the signed boolean", () => {
expect(sponsoredSend.isSigned()).toEqual(true);
});

it("sets the signature", () => {
expect(sponsoredSend.getSignature().slice(2)).toEqual(signature);
});
});

describe("#getStatus", () => {
it("should return undefined when the SponsoredSend has not been initiated with a model", async () => {
model.status = "";
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.getStatus()).toBeUndefined();
});

it("should return a pending status", () => {
model.status = SponsoredSendStatus.PENDING;
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.getStatus()).toEqual("pending");
});

it("should return a submitted status", () => {
model.status = SponsoredSendStatus.SUBMITTED;
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.getStatus()).toEqual("submitted");
});

it("should return a complete status", () => {
model.status = SponsoredSendStatus.COMPLETE;
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.getStatus()).toEqual("complete");
});

it("should return a failed status", () => {
model.status = SponsoredSendStatus.FAILED;
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.getStatus()).toEqual("failed");
});
});

describe("#isTerminalState", () => {
it("should not be in a terminal state", () => {
expect(sponsoredSend.isTerminalState()).toEqual(false);
});

it("should be in a terminal state", () => {
model.status = SponsoredSendStatus.COMPLETE;
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.isTerminalState()).toEqual(true);
});

it("should not be in a terminal state with an undefined status", () => {
model.status = "foo-status";
const sponsoredSend = new SponsoredSend(model);
expect(sponsoredSend.isTerminalState()).toEqual(false);
});
});

describe("#toString", () => {
it("includes SponsoredSend details", () => {
const sponsoredSend = new SponsoredSend(completedModel);
expect(sponsoredSend.toString()).toContain(sponsoredSend.getStatus());
});

it("returns the same value as toString", () => {
const sponsoredSend = new SponsoredSend(completedModel);
expect(sponsoredSend.toString()).toEqual(
`SponsoredSend { transactionHash: '${sponsoredSend.getTransactionHash()}', status: '${sponsoredSend.getStatus()}', typedDataHash: '${sponsoredSend.getTypedDataHash()}', signature: ${sponsoredSend.getSignature()}, transactionLink: ${sponsoredSend.getTransactionLink()} }`,
);
});

it("should include the transaction hash when the SponsoredSend has been broadcast on chain", () => {
const sponsoredSend = new SponsoredSend(completedModel);
expect(sponsoredSend.toString()).toContain(sponsoredSend.getTransactionHash());
});
});
});
Loading