diff --git a/CHANGELOG.md b/CHANGELOG.md index da5c3bf3..03e67048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Coinbase Node.js SDK Changelog ## Unreleased +- Add support for fetching address reputation + - Add `reputation` method to `Address` to fetch the reputation of the address. + +## [0.12.0] - Skipped ### [0.11.3] - 2024-12-10 diff --git a/src/coinbase/address.ts b/src/coinbase/address.ts index 1f40844a..a03fa8fb 100644 --- a/src/coinbase/address.ts +++ b/src/coinbase/address.ts @@ -16,6 +16,7 @@ import { formatDate, getWeekBackDate } from "./utils"; import { StakingReward } from "./staking_reward"; import { StakingBalance } from "./staking_balance"; import { Transaction } from "./transaction"; +import { AddressReputation } from "./address_reputation"; /** * A representation of a blockchain address, which is a user-controlled account on a network. @@ -25,6 +26,7 @@ export class Address { protected networkId: string; protected id: string; + protected _reputation?: AddressReputation; /** * Initializes a new Address instance. @@ -296,6 +298,27 @@ export class Address { return new FaucetTransaction(response.data); } + /** + * Returns the reputation of the Address. + * + * @returns The reputation of the Address. + * @throws {Error} if the API request to get the Address reputation fails. + * @throws {Error} if the Address reputation is not available. + */ + public async reputation(): Promise { + if (this._reputation) { + return this._reputation; + } + + const response = await Coinbase.apiClients.addressReputation!.getAddressReputation( + this.getNetworkId(), + this.getId(), + ); + + this._reputation = new AddressReputation(response.data); + return this._reputation; + } + /** * Returns a string representation of the address. * diff --git a/src/coinbase/address_reputation.ts b/src/coinbase/address_reputation.ts new file mode 100644 index 00000000..d18e9d2c --- /dev/null +++ b/src/coinbase/address_reputation.ts @@ -0,0 +1,60 @@ +import { AddressReputation as AddressReputationModel, AddressReputationMetadata } from "../client"; + +/** + * A representation of the reputation of a blockchain address. + */ +export class AddressReputation { + private model: AddressReputationModel; + /** + * A representation of the reputation of a blockchain address. + * + * @param {AddressReputationModel} model - The reputation model instance. + */ + constructor(model: AddressReputationModel) { + if (!model) { + throw new Error("Address reputation model cannot be empty"); + } + this.model = model; + } + + /** + * Returns the address ID. + * + * @returns {string} The address ID. + */ + public get risky(): boolean { + return this.model.score < 0; + } + + /** + * Returns the score of the address. + * The score is a number between -100 and 100. + * + * @returns {number} The score of the address. + */ + public get score(): number { + return this.model.score; + } + + /** + * Returns the metadata of the address reputation. + * The metadata contains additional information about the address reputation. + * + * @returns {AddressReputationMetadata} The metadata of the address reputation. + */ + public get metadata(): AddressReputationMetadata { + return this.model.metadata; + } + + /** + * Returns the address ID. + * + * @returns {string} The address ID. + */ + toString(): string { + const metadata = Object.entries(this.model.metadata).map(([key, value]) => { + return `${key}: ${value}`; + }); + return `AddressReputation(score: ${this.score}, metadata: {${metadata.join(", ")}})`; + } +} diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 56063596..6b9fd818 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -19,6 +19,7 @@ import { TransactionHistoryApiFactory, MPCWalletStakeApiFactory, FundApiFactory, + ReputationApiFactory, } from "../client"; import { BASE_PATH } from "./../client/base"; import { Configuration } from "./../client/configuration"; @@ -172,6 +173,7 @@ export class Coinbase { ); Coinbase.apiKeyPrivateKey = privateKey; Coinbase.useServerSigner = useServerSigner; + Coinbase.apiClients.addressReputation = ReputationApiFactory(config, basePath, axiosInstance); } /** diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index 0d903000..506d2da7 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -61,6 +61,7 @@ import { FundOperationList, CreateFundOperationRequest, CreateFundQuoteRequest, + AddressReputation, } from "./../client/api"; import { Address } from "./address"; import { Wallet } from "./wallet"; @@ -727,6 +728,7 @@ export type ApiClients = { transactionHistory?: TransactionHistoryApiClient; smartContract?: SmartContractAPIClient; fund?: FundOperationApiClient; + addressReputation?: AddressReputationApiClient; }; /** @@ -1503,6 +1505,22 @@ export interface FundOperationApiClient { ): AxiosPromise; } +export interface AddressReputationApiClient { + /** + * Get the reputation of an address + * + * @param networkId - The ID of the blockchain network + * @param addressId - The ID of the address to fetch the reputation for + * @param options - Override http request option. + * @throws {APIError} If the request fails. + */ + getAddressReputation( + networkId: string, + addressId: string, + options?: RawAxiosRequestConfig, + ): AxiosPromise; +} + /** * Options for pagination on list methods. */ diff --git a/src/tests/address_reputation_test.ts b/src/tests/address_reputation_test.ts new file mode 100644 index 00000000..71cac006 --- /dev/null +++ b/src/tests/address_reputation_test.ts @@ -0,0 +1,96 @@ +import { AddressReputation } from "../coinbase/address_reputation"; + +describe("AddressReputation", () => { + let addressReputation: AddressReputation; + + beforeEach(() => { + addressReputation = new AddressReputation({ + score: -90, + metadata: { + unique_days_active: 1, + total_transactions: 1, + token_swaps_performed: 1, + bridge_transactions_performed: 1, + smart_contract_deployments: 1, + longest_active_streak: 1, + lend_borrow_stake_transactions: 1, + ens_contract_interactions: 1, + current_active_streak: 1, + activity_period_days: 1, + }, + }); + }); + + it("returns the score", () => { + expect(addressReputation.score).toBe(-90); + }); + + it("returns the metadata", () => { + expect(addressReputation.metadata).toEqual({ + unique_days_active: 1, + total_transactions: 1, + token_swaps_performed: 1, + bridge_transactions_performed: 1, + smart_contract_deployments: 1, + longest_active_streak: 1, + lend_borrow_stake_transactions: 1, + ens_contract_interactions: 1, + current_active_streak: 1, + activity_period_days: 1, + }); + }); + + it("returns the string representation of the address reputation", () => { + expect(addressReputation.toString()).toBe( + "AddressReputation(score: -90, metadata: {unique_days_active: 1, total_transactions: 1, token_swaps_performed: 1, bridge_transactions_performed: 1, smart_contract_deployments: 1, longest_active_streak: 1, lend_borrow_stake_transactions: 1, ens_contract_interactions: 1, current_active_streak: 1, activity_period_days: 1})", + ); + }); + + it("should throw an error for an empty model", () => { + expect(() => new AddressReputation(null!)).toThrow("Address reputation model cannot be empty"); + }); + + describe("#risky", () => { + it("returns the risky as true for score < 0", () => { + expect(addressReputation.risky).toBe(true); + }); + + it("should return risky as false for a score > 0", () => { + addressReputation = new AddressReputation({ + score: 90, + metadata: { + unique_days_active: 1, + total_transactions: 1, + token_swaps_performed: 1, + bridge_transactions_performed: 1, + smart_contract_deployments: 1, + longest_active_streak: 1, + lend_borrow_stake_transactions: 1, + ens_contract_interactions: 1, + current_active_streak: 1, + activity_period_days: 1, + }, + }); + expect(addressReputation.risky).toBe(false); + }); + + it("should return risky as false for a score=0", () => { + addressReputation = new AddressReputation({ + score: 0, + metadata: { + unique_days_active: 1, + total_transactions: 1, + token_swaps_performed: 1, + bridge_transactions_performed: 1, + smart_contract_deployments: 1, + longest_active_streak: 1, + lend_borrow_stake_transactions: 1, + ens_contract_interactions: 1, + current_active_streak: 1, + activity_period_days: 1, + }, + }); + expect(addressReputation.risky).toBe(false); + }); + }); +}); diff --git a/src/tests/address_test.ts b/src/tests/address_test.ts index 8b4fc092..fb02a761 100644 --- a/src/tests/address_test.ts +++ b/src/tests/address_test.ts @@ -1,12 +1,13 @@ import { Coinbase } from "../coinbase/coinbase"; import { Address, TransactionStatus } from "../index"; -import { AddressHistoricalBalanceList, AddressTransactionList } from "../client"; +import { AddressHistoricalBalanceList, AddressTransactionList, AddressReputation } from "../client"; import { VALID_ADDRESS_MODEL, mockReturnValue, newAddressModel, balanceHistoryApiMock, transactionHistoryApiMock, + reputationApiMock, } from "./utils"; import Decimal from "decimal.js"; import { randomUUID } from "crypto"; @@ -269,4 +270,49 @@ describe("Address", () => { expect(paginationResponse.nextPage).toEqual("next page"); }); }); + + describe("#reputation", () => { + beforeEach(() => { + const mockReputationResponse: AddressReputation = { + score: 90, + metadata: { + activity_period_days: 1, + bridge_transactions_performed: 1, + current_active_streak: 1, + ens_contract_interactions: 2, + lend_borrow_stake_transactions: 3, + longest_active_streak: 4, + smart_contract_deployments: 5, + token_swaps_performed: 6, + total_transactions: 7, + unique_days_active: 8, + }, + }; + Coinbase.apiClients.addressReputation = reputationApiMock; + Coinbase.apiClients.addressReputation!.getAddressReputation = + mockReturnValue(mockReputationResponse); + }); + + it("should return address reputation", async () => { + const reputation = await address.reputation(); + expect(reputation.score).toEqual(90); + expect(reputation.metadata).toEqual({ + activity_period_days: 1, + bridge_transactions_performed: 1, + current_active_streak: 1, + ens_contract_interactions: 2, + lend_borrow_stake_transactions: 3, + longest_active_streak: 4, + smart_contract_deployments: 5, + token_swaps_performed: 6, + total_transactions: 7, + unique_days_active: 8, + }); + expect(Coinbase.apiClients.addressReputation!.getAddressReputation).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.addressReputation!.getAddressReputation).toHaveBeenCalledWith( + address.getNetworkId(), + address.getId(), + ); + }); + }); }); diff --git a/src/tests/utils.ts b/src/tests/utils.ts index 3c9ead03..0b35a2f5 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -801,6 +801,10 @@ export const fundOperationsApiMock = { createFundQuote: jest.fn(), }; +export const reputationApiMock = { + getAddressReputation: jest.fn(), +}; + export const testAllReadTypesABI = [ { type: "function",