diff --git a/Dockerfile b/Dockerfile index beaa840fb3..a465116eca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -136,6 +136,7 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-eos /var/modules/sdk-coin-eos/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-ethlike /var/modules/sdk-coin-ethlike/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-ethw /var/modules/sdk-coin-ethw/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-iota /var/modules/sdk-coin-iota/ +COPY --from=builder /tmp/bitgo/modules/sdk-coin-irys /var/modules/sdk-coin-irys/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-lnbtc /var/modules/sdk-coin-lnbtc/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-ltc /var/modules/sdk-coin-ltc/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-xlm /var/modules/sdk-coin-xlm/ @@ -236,6 +237,7 @@ cd /var/modules/sdk-coin-eos && yarn link && \ cd /var/modules/sdk-coin-ethlike && yarn link && \ cd /var/modules/sdk-coin-ethw && yarn link && \ cd /var/modules/sdk-coin-iota && yarn link && \ +cd /var/modules/sdk-coin-irys && yarn link && \ cd /var/modules/sdk-coin-lnbtc && yarn link && \ cd /var/modules/sdk-coin-ltc && yarn link && \ cd /var/modules/sdk-coin-xlm && yarn link && \ @@ -339,6 +341,7 @@ RUN cd /var/bitgo-express && \ yarn link @bitgo/sdk-coin-ethlike && \ yarn link @bitgo/sdk-coin-ethw && \ yarn link @bitgo/sdk-coin-iota && \ + yarn link @bitgo/sdk-coin-irys && \ yarn link @bitgo/sdk-coin-lnbtc && \ yarn link @bitgo/sdk-coin-ltc && \ yarn link @bitgo/sdk-coin-xlm && \ diff --git a/modules/bitgo/package.json b/modules/bitgo/package.json index fc6ab07776..c82951d08d 100644 --- a/modules/bitgo/package.json +++ b/modules/bitgo/package.json @@ -90,6 +90,7 @@ "@bitgo/sdk-coin-initia": "^2.5.2", "@bitgo/sdk-coin-injective": "^3.6.2", "@bitgo/sdk-coin-iota": "^1.8.2", + "@bitgo/sdk-coin-irys": "^1.0.0", "@bitgo/sdk-coin-islm": "^2.5.2", "@bitgo/sdk-coin-lnbtc": "^1.6.2", "@bitgo/sdk-coin-ltc": "^3.7.2", diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index f48257905f..89a7a7e562 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -105,6 +105,7 @@ import { Initia, Injective, Iota, + Irys, Islm, JettonToken, Lnbtc, @@ -183,6 +184,7 @@ import { Ticp, Tinitia, Tinjective, + TIrys, Tislm, Tlnbtc, Tltc, @@ -293,6 +295,7 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin coinFactory.register('initia', Initia.createInstance); coinFactory.register('injective', Injective.createInstance); coinFactory.register('iota', Iota.createInstance); + coinFactory.register('irys', Irys.createInstance); coinFactory.register('islm', Islm.createInstance); coinFactory.register('near', Near.createInstance); coinFactory.register('oas', Oas.createInstance); @@ -361,6 +364,7 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin coinFactory.register('tinitia', Tinitia.createInstance); coinFactory.register('tinjective', Tinjective.createInstance); coinFactory.register('tiota', Iota.createInstance); + coinFactory.register('tirys', TIrys.createInstance); coinFactory.register('tislm', Tislm.createInstance); coinFactory.register('tlnbtc', Tlnbtc.createInstance); coinFactory.register('tltc', Tltc.createInstance); diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 606c54f97c..41af59f68d 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -42,6 +42,7 @@ import { Icp, Ticp } from '@bitgo/sdk-coin-icp'; import { Initia, Tinitia } from '@bitgo/sdk-coin-initia'; import { Injective, Tinjective } from '@bitgo/sdk-coin-injective'; import { Iota } from '@bitgo/sdk-coin-iota'; +import { Irys, TIrys } from '@bitgo/sdk-coin-irys'; import { Islm, Tislm } from '@bitgo/sdk-coin-islm'; import { Lnbtc, Tlnbtc } from '@bitgo/sdk-coin-lnbtc'; import { Ltc, Tltc } from '@bitgo/sdk-coin-ltc'; @@ -118,6 +119,7 @@ export { Hbar, Thbar }; export { Icp, Ticp }; export { Initia, Tinitia }; export { Iota }; +export { Irys, TIrys }; export { Lnbtc, Tlnbtc }; export { Ltc, Tltc }; export { Mon, Tmon, MonToken }; diff --git a/modules/bitgo/tsconfig.json b/modules/bitgo/tsconfig.json index 5f24815255..e5695543ed 100644 --- a/modules/bitgo/tsconfig.json +++ b/modules/bitgo/tsconfig.json @@ -182,6 +182,9 @@ { "path": "../sdk-coin-iota" }, + { + "path": "../sdk-coin-irys" + }, { "path": "../sdk-coin-islm" }, diff --git a/modules/sdk-coin-irys/.mocharc.yml b/modules/sdk-coin-irys/.mocharc.yml new file mode 100644 index 0000000000..f499ec0a83 --- /dev/null +++ b/modules/sdk-coin-irys/.mocharc.yml @@ -0,0 +1,8 @@ +require: 'tsx' +timeout: '60000' +reporter: 'min' +reporter-option: + - 'cdn=true' + - 'json=false' +exit: true +spec: ['test/unit/**/*.ts'] diff --git a/modules/sdk-coin-irys/.npmignore b/modules/sdk-coin-irys/.npmignore new file mode 100644 index 0000000000..d5fb3a098c --- /dev/null +++ b/modules/sdk-coin-irys/.npmignore @@ -0,0 +1,14 @@ +!dist/ +dist/test/ +dist/tsconfig.tsbuildinfo +.idea/ +.prettierrc.yml +tsconfig.json +src/ +test/ +scripts/ +.nyc_output +CODEOWNERS +node_modules/ +.prettierignore +.mocharc.js diff --git a/modules/sdk-coin-irys/package.json b/modules/sdk-coin-irys/package.json new file mode 100644 index 0000000000..1edc7b2c9a --- /dev/null +++ b/modules/sdk-coin-irys/package.json @@ -0,0 +1,64 @@ +{ + "name": "@bitgo/sdk-coin-irys", + "version": "1.0.0", + "description": "BitGo SDK coin library for Irys", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "yarn tsc --build --incremental --verbose .", + "fmt": "prettier --write .", + "check-fmt": "prettier --check '**/*.{ts,js,json}'", + "clean": "rm -r ./dist", + "lint": "eslint --quiet .", + "prepare": "npm run build", + "test": "npm run coverage", + "coverage": "nyc -- npm run unit-test", + "unit-test": "mocha" + }, + "author": "BitGo SDK Team ", + "license": "MIT", + "engines": { + "node": ">=20 <25" + }, + "repository": { + "type": "git", + "url": "https://github.com/BitGo/BitGoJS.git", + "directory": "modules/sdk-coin-irys" + }, + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "extension": [ + ".ts" + ] + }, + "dependencies": { + "@bitgo/abstract-eth": "^24.20.2", + "@bitgo/sdk-core": "^36.31.1", + "@bitgo/statics": "^58.25.0", + "@ethereumjs/common": "^2.6.5", + "@ethereumjs/rlp": "^4.0.0", + "bs58": "^4.0.1", + "ethers": "^5.1.3", + "superagent": "^9.0.1" + }, + "devDependencies": { + "@bitgo/sdk-api": "^1.74.1", + "@bitgo/sdk-test": "^9.1.27", + "@types/sinon": "^10.0.11", + "@types/superagent": "^8.1.0", + "nock": "^13.3.1", + "should": "^13.2.3", + "sinon": "^13.0.1" + }, + "files": [ + "dist" + ] +} diff --git a/modules/sdk-coin-irys/src/index.ts b/modules/sdk-coin-irys/src/index.ts new file mode 100644 index 0000000000..8557176b51 --- /dev/null +++ b/modules/sdk-coin-irys/src/index.ts @@ -0,0 +1,4 @@ +export * from './irys'; +export * from './tirys'; +export * from './register'; +export * from './lib'; diff --git a/modules/sdk-coin-irys/src/irys.ts b/modules/sdk-coin-irys/src/irys.ts new file mode 100644 index 0000000000..d152669a7a --- /dev/null +++ b/modules/sdk-coin-irys/src/irys.ts @@ -0,0 +1,64 @@ +import { BaseCoin, BitGoBase, MPCAlgorithm } from '@bitgo/sdk-core'; +import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth'; +import { CoinFeature, BaseCoin as StaticsBaseCoin, coins, EthereumNetwork } from '@bitgo/statics'; +import { IrysCommitmentTransactionBuilder, TransactionBuilder } from './lib'; + +/** + * Irys coin implementation. + * + * Irys is EVM-compatible for standard transfers (inherits from AbstractEthLikeNewCoins) + * but uses custom commitment transactions for staking (STAKE, PLEDGE, etc.). + * + * Standard EVM operations (transfers, balance queries) use the inherited EVM logic. + * Commitment transactions use the IrysCommitmentTransactionBuilder. + */ +export class Irys extends AbstractEthLikeNewCoins { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new Irys(bitgo, staticsCoin); + } + + /** + * Irys supports TSS (from EVM_FEATURES in statics). + */ + supportsTss(): boolean { + return this.staticsCoin?.features.includes(CoinFeature.TSS) ?? false; + } + + /** @inheritdoc */ + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + /** + * Get the Irys native API URL from the network config. + * This is the non-EVM API used for commitment transactions. + */ + getIrysApiUrl(): string | undefined { + const network = this.getNetwork() as EthereumNetwork; + return network.irysApiUrl; + } + + /** + * Create a commitment transaction builder for staking operations. + * This is separate from getTransactionBuilder() which handles standard EVM transfers. + */ + getCommitmentTransactionBuilder(): IrysCommitmentTransactionBuilder { + const apiUrl = this.getIrysApiUrl(); + if (!apiUrl) { + throw new Error('Irys API URL is not configured for this network'); + } + return new IrysCommitmentTransactionBuilder(apiUrl, BigInt(this.getChainId())); + } + + /** + * Create a new transaction builder for standard EVM transactions. + * @return a new transaction builder + */ + protected getTransactionBuilder(): TransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } +} diff --git a/modules/sdk-coin-irys/src/lib/commitmentTransactionBuilder.ts b/modules/sdk-coin-irys/src/lib/commitmentTransactionBuilder.ts new file mode 100644 index 0000000000..4728424e3c --- /dev/null +++ b/modules/sdk-coin-irys/src/lib/commitmentTransactionBuilder.ts @@ -0,0 +1,253 @@ +import { RLP } from '@ethereumjs/rlp'; +import { arrayify, keccak256 } from 'ethers/lib/utils'; +import request from 'superagent'; +import { + CommitmentType, + CommitmentTypeId, + CommitmentTransactionFields, + CommitmentTransactionBuildResult, + EncodedSignedCommitmentTransaction, + EncodedCommitmentType, + AnchorInfo, + COMMITMENT_TX_VERSION, +} from './iface'; +import { encodeBase58, decodeBase58ToFixed } from './utils'; + +/** + * Builder for Irys commitment transactions (STAKE, PLEDGE). + * + * Commitment transactions are NOT standard EVM transactions. They use a custom + * 7-field RLP encoding with keccak256 prehash and raw ECDSA signing. + * + * Usage (STAKE): + * const builder = new IrysCommitmentTransactionBuilder(apiUrl, chainId); + * builder.setCommitmentType({ type: CommitmentTypeId.STAKE }); + * builder.setFee(fee); + * builder.setValue(value); + * builder.setSigner(signerAddress); + * const result = await builder.build(); // fetches anchor, RLP encodes, returns prehash + * + * Usage (PLEDGE): + * builder.setCommitmentType({ type: CommitmentTypeId.PLEDGE, pledgeCount: 0n }); + */ +export class IrysCommitmentTransactionBuilder { + private _irysApiUrl: string; + private _chainId: bigint; + private _commitmentType: CommitmentType; + private _fee: bigint; + private _value: bigint; + private _signer: Uint8Array; // 20 bytes + private _anchor: Uint8Array; // 32 bytes (set during build, or manually for testing) + + constructor(irysApiUrl: string, chainId: bigint) { + this._irysApiUrl = irysApiUrl; + this._chainId = chainId; + } + + /** + * Set the commitment type for this transaction. + * STAKE is a single-operation type. + * PLEDGE requires pledgeCount. + */ + setCommitmentType(type: CommitmentType): this { + this._commitmentType = type; + return this; + } + + /** Set the transaction fee (from Irys price API) */ + setFee(fee: bigint): this { + this._fee = fee; + return this; + } + + /** Set the transaction value (from Irys price API) */ + setValue(value: bigint): this { + this._value = value; + return this; + } + + /** Set the signer address (20-byte Ethereum address as Uint8Array) */ + setSigner(signer: Uint8Array): this { + if (signer.length !== 20) { + throw new Error(`Signer must be 20 bytes, got ${signer.length}`); + } + this._signer = signer; + return this; + } + + /** + * Manually set the anchor (for testing). If not set, build() fetches it from the API. + */ + setAnchor(anchor: Uint8Array): this { + if (anchor.length !== 32) { + throw new Error(`Anchor must be 32 bytes, got ${anchor.length}`); + } + this._anchor = anchor; + return this; + } + + /** + * Fetch the current anchor (block hash) from the Irys API. + * This is the nonce equivalent for commitment transactions. + * Called during build() if anchor hasn't been manually set. + */ + async fetchAnchor(): Promise { + const response = await request.get(`${this._irysApiUrl}/anchor`).accept('json'); + + if (!response.ok) { + throw new Error(`Failed to fetch anchor: ${response.status} ${response.text}`); + } + + const anchorInfo: AnchorInfo = response.body; + return decodeBase58ToFixed(anchorInfo.blockHash, 32); + } + + /** + * Encode the commitment type for RLP signing. + * + * CRITICAL: STAKE (1) MUST be a flat number, NOT an array. + * PLEDGE MUST be a nested array. The Irys Rust decoder + * rejects non-canonical encoding. + * + * Reference: irys-js/src/common/commitmentTransaction.ts lines 180-199 + */ + static encodeCommitmentTypeForSigning( + type: CommitmentType + ): number | bigint | Uint8Array | (number | bigint | Uint8Array)[] { + switch (type.type) { + case CommitmentTypeId.STAKE: + return CommitmentTypeId.STAKE; // flat number + case CommitmentTypeId.PLEDGE: + return [CommitmentTypeId.PLEDGE, type.pledgeCount]; // nested array + default: + throw new Error(`Unknown commitment type`); + } + } + + /** + * Encode the commitment type for the JSON broadcast payload. + */ + static encodeCommitmentTypeForBroadcast(type: CommitmentType): EncodedCommitmentType { + switch (type.type) { + case CommitmentTypeId.STAKE: + return { type: 'stake' }; + case CommitmentTypeId.PLEDGE: + return { type: 'pledge', pledgeCountBeforeExecuting: type.pledgeCount.toString() }; + default: + throw new Error(`Unknown commitment type`); + } + } + + /** + * Validate that all required fields are set before building. + */ + private validateFields(): void { + if (!this._commitmentType) throw new Error('Commitment type is required'); + if (this._fee === undefined) throw new Error('Fee is required'); + if (this._value === undefined) throw new Error('Value is required'); + if (!this._signer) throw new Error('Signer is required'); + } + + /** + * Build the unsigned commitment transaction. + * + * 1. Validates all fields are set + * 2. Fetches anchor from Irys API (if not manually set) -- done LAST to minimize expiration + * 3. RLP encodes the 7 fields in exact order + * 4. Computes keccak256 prehash + * 5. Returns prehash (for HSM) and rlpEncoded (for HSM validation) + */ + async build(): Promise { + this.validateFields(); + + // Fetch anchor LAST -- it expires in ~45 blocks (~9 min) + if (!this._anchor) { + this._anchor = await this.fetchAnchor(); + } + + const fields: CommitmentTransactionFields = { + version: COMMITMENT_TX_VERSION, + anchor: this._anchor, + signer: this._signer, + commitmentType: this._commitmentType, + chainId: this._chainId, + fee: this._fee, + value: this._value, + }; + + const rlpEncoded = this.rlpEncode(fields); + const prehash = this.computePrehash(rlpEncoded); + + return { prehash, rlpEncoded, fields }; + } + + /** + * RLP encode the 7 commitment transaction fields. + * + * Field order is CRITICAL and must match the Irys protocol exactly: + * [version, anchor, signer, commitmentType, chainId, fee, value] + * + * Reference: irys-js/src/common/commitmentTransaction.ts lines 405-419 + */ + rlpEncode(fields: CommitmentTransactionFields): Uint8Array { + const rlpFields = [ + fields.version, + fields.anchor, + fields.signer, + IrysCommitmentTransactionBuilder.encodeCommitmentTypeForSigning(fields.commitmentType), + fields.chainId, + fields.fee, + fields.value, + ]; + + return RLP.encode(rlpFields as any); + } + + /** + * Compute the prehash: keccak256(rlpEncoded). + * Returns 32 bytes. + */ + computePrehash(rlpEncoded: Uint8Array): Uint8Array { + const hash = keccak256(rlpEncoded); + return arrayify(hash); + } + + /** + * Compute the transaction ID from a signature. + * txId = base58(keccak256(signature)) + * + * @param signature - 65-byte raw ECDSA signature (r || s || v) + */ + static computeTxId(signature: Uint8Array): string { + if (signature.length !== 65) { + throw new Error(`Signature must be 65 bytes, got ${signature.length}`); + } + const idBytes = arrayify(keccak256(signature)); + return encodeBase58(idBytes); + } + + /** + * Create the JSON broadcast payload from a signed transaction. + * + * @param fields - The transaction fields used to build the transaction + * @param signature - 65-byte raw ECDSA signature + * @returns JSON payload ready for POST /v1/commitment-tx + */ + static createBroadcastPayload( + fields: CommitmentTransactionFields, + signature: Uint8Array + ): EncodedSignedCommitmentTransaction { + const txId = IrysCommitmentTransactionBuilder.computeTxId(signature); + return { + version: fields.version, + anchor: encodeBase58(fields.anchor), + signer: encodeBase58(fields.signer), + commitmentType: IrysCommitmentTransactionBuilder.encodeCommitmentTypeForBroadcast(fields.commitmentType), + chainId: fields.chainId.toString(), + fee: fields.fee.toString(), + value: fields.value.toString(), + id: txId, + signature: encodeBase58(signature), + }; + } +} diff --git a/modules/sdk-coin-irys/src/lib/iface.ts b/modules/sdk-coin-irys/src/lib/iface.ts new file mode 100644 index 0000000000..122f01742d --- /dev/null +++ b/modules/sdk-coin-irys/src/lib/iface.ts @@ -0,0 +1,85 @@ +/** + * Commitment type IDs matching the Irys protocol. + * STAKE is a flat value in RLP encoding. + * PLEDGE is encoded as a nested array. + */ +export enum CommitmentTypeId { + STAKE = 1, + PLEDGE = 2, +} + +export type StakeCommitmentType = { type: CommitmentTypeId.STAKE }; +export type PledgeCommitmentType = { type: CommitmentTypeId.PLEDGE; pledgeCount: bigint }; + +export type CommitmentType = StakeCommitmentType | PledgeCommitmentType; + +/** Version 2 is the current commitment transaction version */ +export const COMMITMENT_TX_VERSION = 2; + +/** Irys chain IDs */ +export const IRYS_MAINNET_CHAIN_ID = 3282n; +export const IRYS_TESTNET_CHAIN_ID = 1270n; + +/** + * The 7 fields of an unsigned commitment transaction, + * in the exact order required for RLP encoding. + */ +export interface CommitmentTransactionFields { + version: number; // 1 byte, always 2 (V2) + anchor: Uint8Array; // 32 bytes (block hash from /v1/anchor) + signer: Uint8Array; // 20 bytes (Ethereum address) + commitmentType: CommitmentType; + chainId: bigint; + fee: bigint; + value: bigint; +} + +/** + * JSON payload for broadcasting a signed commitment transaction + * via POST /v1/commitment-tx + */ +export interface EncodedSignedCommitmentTransaction { + version: number; + anchor: string; // base58 + signer: string; // base58 + commitmentType: EncodedCommitmentType; + chainId: string; // decimal string + fee: string; // decimal string + value: string; // decimal string + id: string; // base58(keccak256(signature)) + signature: string; // base58(65-byte raw signature) +} + +export type EncodedCommitmentType = { type: 'stake' } | { type: 'pledge'; pledgeCountBeforeExecuting: string }; + +/** + * Anchor info returned by GET /v1/anchor + */ +export interface AnchorInfo { + blockHash: string; // base58-encoded 32-byte block hash +} + +/** + * Result of building an unsigned commitment transaction. + * Contains the prehash (for HSM signing) and the RLP-encoded bytes (for HSM validation). + */ +export interface CommitmentTransactionBuildResult { + /** keccak256(rlpEncoded) - 32 bytes, used as prehash for signing */ + prehash: Uint8Array; + /** Full RLP-encoded transaction bytes - sent to HSM for validation before signing */ + rlpEncoded: Uint8Array; + /** The transaction fields used to build this result */ + fields: CommitmentTransactionFields; +} + +/** + * Result after signing. Contains everything needed for broadcast. + */ +export interface SignedCommitmentTransactionResult { + /** Transaction ID: base58(keccak256(signature)) */ + txId: string; + /** 65-byte raw ECDSA signature (r || s || v) */ + signature: Uint8Array; + /** JSON payload ready for POST /v1/commitment-tx */ + broadcastPayload: EncodedSignedCommitmentTransaction; +} diff --git a/modules/sdk-coin-irys/src/lib/index.ts b/modules/sdk-coin-irys/src/lib/index.ts new file mode 100644 index 0000000000..9da76da800 --- /dev/null +++ b/modules/sdk-coin-irys/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from './iface'; +export * from './commitmentTransactionBuilder'; +export * from './transactionBuilder'; +export * from './utils'; diff --git a/modules/sdk-coin-irys/src/lib/transactionBuilder.ts b/modules/sdk-coin-irys/src/lib/transactionBuilder.ts new file mode 100644 index 0000000000..4d81ca01c9 --- /dev/null +++ b/modules/sdk-coin-irys/src/lib/transactionBuilder.ts @@ -0,0 +1,64 @@ +import { BaseCoin as CoinConfig, EthereumNetwork, CoinFeature, NetworkType } from '@bitgo/statics'; +import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilder as AbstractTransactionBuilder, Transaction, TransferBuilder } from '@bitgo/abstract-eth'; +import EthereumCommon from '@ethereumjs/common'; + +/** + * Get the Ethereum common configuration for Irys. + * @param coin - The coin configuration + * @returns Ethereum common configuration object + */ +function getCommon(coin: Readonly): EthereumCommon { + return EthereumCommon.custom( + { + name: coin.network.name, + networkId: (coin.network as EthereumNetwork).chainId, + chainId: (coin.network as EthereumNetwork).chainId, + }, + { + baseChain: coin.network.type === NetworkType.MAINNET ? 'mainnet' : 'sepolia', + hardfork: coin.features.includes(CoinFeature.EIP1559) ? 'london' : undefined, + eips: coin.features.includes(CoinFeature.EIP1559) ? [1559] : undefined, + } + ); +} + +/** + * Irys transaction builder for standard EVM transactions. + */ +export class TransactionBuilder extends AbstractTransactionBuilder { + protected _transfer: TransferBuilder; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._common = getCommon(this._coinConfig); + this.transaction = new Transaction(this._coinConfig, this._common); + } + + /** @inheritdoc */ + transfer(data?: string): TransferBuilder { + if (this._type !== TransactionType.Send) { + throw new BuildTransactionError('Transfers can only be set for send transactions'); + } + if (!this._transfer) { + this._transfer = new TransferBuilder(data); + } + return this._transfer; + } + + /** + * Get contract data for wallet initialization. + * + * This method is intentionally not implemented for Irys. Irys uses commitment + * transactions (STAKE, PLEDGE) for staking operations, which are built via + * IrysCommitmentTransactionBuilder, not through standard EVM contract calls. + * Standard EVM transfers work normally via the inherited transfer() method. + * + * @throws Error - Always throws as this is not supported for Irys + */ + protected getContractData(addresses: string[]): string { + throw new Error( + 'getContractData is not implemented for Irys. Use IrysCommitmentTransactionBuilder for staking operations.' + ); + } +} diff --git a/modules/sdk-coin-irys/src/lib/utils.ts b/modules/sdk-coin-irys/src/lib/utils.ts new file mode 100644 index 0000000000..ca7eae5aea --- /dev/null +++ b/modules/sdk-coin-irys/src/lib/utils.ts @@ -0,0 +1,46 @@ +import bs58 from 'bs58'; + +/** + * Encode a byte array to Base58 string. + * Used for encoding addresses, anchors, and signatures for the Irys API. + */ +export function encodeBase58(bytes: Uint8Array): string { + return bs58.encode(Buffer.from(bytes)); +} + +/** + * Decode a Base58 string to a byte array. + */ +export function decodeBase58(str: string): Uint8Array { + return Uint8Array.from(bs58.decode(str)); +} + +/** + * Decode a Base58 string to a fixed-length byte array. + * Throws if decoded length doesn't match expected length. + */ +export function decodeBase58ToFixed(str: string, expectedLength: number): Uint8Array { + const decoded = bs58.decode(str); + if (decoded.length !== expectedLength) { + throw new Error(`Expected ${expectedLength} bytes, got ${decoded.length}`); + } + return Uint8Array.from(decoded); +} + +/** + * Convert a hex address (0x-prefixed or not) to a 20-byte Uint8Array. + */ +export function hexAddressToBytes(hexAddress: string): Uint8Array { + const cleaned = hexAddress.startsWith('0x') ? hexAddress.slice(2) : hexAddress; + if (cleaned.length !== 40) { + throw new Error(`Invalid hex address length: ${cleaned.length}`); + } + return Uint8Array.from(Buffer.from(cleaned, 'hex')); +} + +/** + * Convert a hex address to Base58 (for Irys API calls). + */ +export function hexAddressToBase58(hexAddress: string): string { + return encodeBase58(hexAddressToBytes(hexAddress)); +} diff --git a/modules/sdk-coin-irys/src/register.ts b/modules/sdk-coin-irys/src/register.ts new file mode 100644 index 0000000000..7688b04705 --- /dev/null +++ b/modules/sdk-coin-irys/src/register.ts @@ -0,0 +1,8 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { Irys } from './irys'; +import { TIrys } from './tirys'; + +export const register = (sdk: BitGoBase): void => { + sdk.register('irys', Irys.createInstance); + sdk.register('tirys', TIrys.createInstance); +}; diff --git a/modules/sdk-coin-irys/src/tirys.ts b/modules/sdk-coin-irys/src/tirys.ts new file mode 100644 index 0000000000..9101981151 --- /dev/null +++ b/modules/sdk-coin-irys/src/tirys.ts @@ -0,0 +1,16 @@ +import { BaseCoin, BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { Irys } from './irys'; + +/** + * Irys Testnet coin implementation. + */ +export class TIrys extends Irys { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new TIrys(bitgo, staticsCoin); + } +} diff --git a/modules/sdk-coin-irys/test/unit/commitmentTransactionBuilder.ts b/modules/sdk-coin-irys/test/unit/commitmentTransactionBuilder.ts new file mode 100644 index 0000000000..a55d33dc9b --- /dev/null +++ b/modules/sdk-coin-irys/test/unit/commitmentTransactionBuilder.ts @@ -0,0 +1,444 @@ +import should from 'should'; +import * as sinon from 'sinon'; +import nock from 'nock'; +import { IrysCommitmentTransactionBuilder } from '../../src/lib/commitmentTransactionBuilder'; +import { CommitmentTypeId, COMMITMENT_TX_VERSION, IRYS_TESTNET_CHAIN_ID } from '../../src/lib/iface'; +import { encodeBase58, decodeBase58 } from '../../src/lib/utils'; + +describe('IrysCommitmentTransactionBuilder', function () { + // Common test fixtures + const testAnchor = new Uint8Array(32).fill(1); // 32 bytes of 0x01 + const testSigner = new Uint8Array(20).fill(2); // 20 bytes of 0x02 + const testChainId = IRYS_TESTNET_CHAIN_ID; // 1270n + const testFee = 1000n; + const testValue = 5000n; + const testApiUrl = 'https://testnet-node1.irys.xyz/v1'; + + let builder: IrysCommitmentTransactionBuilder; + + beforeEach(function () { + builder = new IrysCommitmentTransactionBuilder(testApiUrl, testChainId); + }); + + afterEach(function () { + sinon.restore(); + nock.cleanAll(); + }); + + // === Commitment Type Encoding Tests === + + describe('encodeCommitmentTypeForSigning', function () { + it('should encode STAKE as a flat number (not array)', function () { + const result = IrysCommitmentTransactionBuilder.encodeCommitmentTypeForSigning({ + type: CommitmentTypeId.STAKE, + }); + result.should.equal(1); + Array.isArray(result).should.be.false(); + }); + + it('should encode PLEDGE as a nested array [type, pledgeCount]', function () { + const result = IrysCommitmentTransactionBuilder.encodeCommitmentTypeForSigning({ + type: CommitmentTypeId.PLEDGE, + pledgeCount: 42n, + }); + Array.isArray(result).should.be.true(); + (result as any[]).length.should.equal(2); + (result as any[])[0].should.equal(2); + (result as any[])[1].should.equal(42n); + }); + }); + + // === Commitment Type Broadcast Encoding Tests === + + describe('encodeCommitmentTypeForBroadcast', function () { + it('should encode STAKE as { type: "stake" }', function () { + const result = IrysCommitmentTransactionBuilder.encodeCommitmentTypeForBroadcast({ + type: CommitmentTypeId.STAKE, + }); + should.deepEqual(result, { type: 'stake' }); + }); + + it('should encode PLEDGE with pledgeCountBeforeExecuting', function () { + const result = IrysCommitmentTransactionBuilder.encodeCommitmentTypeForBroadcast({ + type: CommitmentTypeId.PLEDGE, + pledgeCount: 42n, + }); + should.deepEqual(result, { type: 'pledge', pledgeCountBeforeExecuting: '42' }); + }); + }); + + // === RLP Encoding Tests === + + describe('rlpEncode', function () { + it('should RLP encode a STAKE transaction with correct field order', function () { + const fields = { + version: COMMITMENT_TX_VERSION, + anchor: testAnchor, + signer: testSigner, + commitmentType: { type: CommitmentTypeId.STAKE as const }, + chainId: testChainId, + fee: testFee, + value: testValue, + }; + + const encoded = builder.rlpEncode(fields); + encoded.should.be.instanceOf(Uint8Array); + encoded.length.should.be.greaterThan(0); + + // The encoded output should be deterministic + const encoded2 = builder.rlpEncode(fields); + Buffer.from(encoded).equals(Buffer.from(encoded2)).should.be.true(); + }); + + it('should RLP encode a PLEDGE transaction with nested array commitment type', function () { + const fields = { + version: COMMITMENT_TX_VERSION, + anchor: testAnchor, + signer: testSigner, + commitmentType: { type: CommitmentTypeId.PLEDGE as const, pledgeCount: 42n }, + chainId: testChainId, + fee: testFee, + value: testValue, + }; + + const encoded = builder.rlpEncode(fields); + encoded.should.be.instanceOf(Uint8Array); + encoded.length.should.be.greaterThan(0); + }); + + it('should produce different encodings for STAKE vs PLEDGE', function () { + const stakeFields = { + version: COMMITMENT_TX_VERSION, + anchor: testAnchor, + signer: testSigner, + commitmentType: { type: CommitmentTypeId.STAKE as const }, + chainId: testChainId, + fee: testFee, + value: testValue, + }; + + const pledgeFields = { + ...stakeFields, + commitmentType: { type: CommitmentTypeId.PLEDGE as const, pledgeCount: 1n }, + }; + + const stakeEncoded = builder.rlpEncode(stakeFields); + const pledgeEncoded = builder.rlpEncode(pledgeFields); + Buffer.from(stakeEncoded).equals(Buffer.from(pledgeEncoded)).should.be.false(); + }); + }); + + // === Prehash Tests === + + describe('computePrehash', function () { + it('should return a 32-byte keccak256 hash', function () { + const rlpEncoded = new Uint8Array([0xc0]); // minimal RLP + const prehash = builder.computePrehash(rlpEncoded); + prehash.should.be.instanceOf(Uint8Array); + prehash.length.should.equal(32); + }); + + it('should produce deterministic output', function () { + const rlpEncoded = new Uint8Array([0xc8, 0x02, 0x01, 0x02, 0x03]); + const hash1 = builder.computePrehash(rlpEncoded); + const hash2 = builder.computePrehash(rlpEncoded); + Buffer.from(hash1).equals(Buffer.from(hash2)).should.be.true(); + }); + + it('should produce different hashes for different inputs', function () { + const input1 = new Uint8Array([0x01]); + const input2 = new Uint8Array([0x02]); + const hash1 = builder.computePrehash(input1); + const hash2 = builder.computePrehash(input2); + Buffer.from(hash1).equals(Buffer.from(hash2)).should.be.false(); + }); + }); + + // === Build Tests === + + describe('build', function () { + it('should build a STAKE transaction with manually set anchor', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(testFee) + .setValue(testValue) + .setSigner(testSigner) + .setAnchor(testAnchor); + + const result = await builder.build(); + result.prehash.should.be.instanceOf(Uint8Array); + result.prehash.length.should.equal(32); + result.rlpEncoded.should.be.instanceOf(Uint8Array); + result.rlpEncoded.length.should.be.greaterThan(0); + result.fields.version.should.equal(COMMITMENT_TX_VERSION); + result.fields.chainId.should.equal(testChainId); + }); + + it('should build a PLEDGE transaction with manually set anchor', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.PLEDGE, pledgeCount: 5n }) + .setFee(testFee) + .setValue(testValue) + .setSigner(testSigner) + .setAnchor(testAnchor); + + const result = await builder.build(); + result.prehash.length.should.equal(32); + result.fields.commitmentType.should.deepEqual({ type: CommitmentTypeId.PLEDGE, pledgeCount: 5n }); + }); + + it('should fetch anchor from API when not manually set', async function () { + const mockAnchorBase58 = encodeBase58(testAnchor); + const scope = nock('https://testnet-node1.irys.xyz') + .get('/v1/anchor') + .reply(200, { blockHash: mockAnchorBase58 }); + + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(testFee) + .setValue(testValue) + .setSigner(testSigner); + + const result = await builder.build(); + result.prehash.length.should.equal(32); + Buffer.from(result.fields.anchor).equals(Buffer.from(testAnchor)).should.be.true(); + scope.done(); + }); + + it('should throw if commitment type is not set', async function () { + builder.setFee(testFee).setValue(testValue).setSigner(testSigner).setAnchor(testAnchor); + + await builder.build().should.be.rejectedWith('Commitment type is required'); + }); + + it('should throw if fee is not set', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setValue(testValue) + .setSigner(testSigner) + .setAnchor(testAnchor); + + await builder.build().should.be.rejectedWith('Fee is required'); + }); + + it('should throw if value is not set', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(testFee) + .setSigner(testSigner) + .setAnchor(testAnchor); + + await builder.build().should.be.rejectedWith('Value is required'); + }); + + it('should throw if signer is not set', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(testFee) + .setValue(testValue) + .setAnchor(testAnchor); + + await builder.build().should.be.rejectedWith('Signer is required'); + }); + }); + + // === Validation Tests === + + describe('input validation', function () { + it('should reject signer with wrong length', function () { + (() => builder.setSigner(new Uint8Array(19))).should.throw(/Signer must be 20 bytes/); + (() => builder.setSigner(new Uint8Array(21))).should.throw(/Signer must be 20 bytes/); + }); + + it('should reject anchor with wrong length', function () { + (() => builder.setAnchor(new Uint8Array(31))).should.throw(/Anchor must be 32 bytes/); + (() => builder.setAnchor(new Uint8Array(33))).should.throw(/Anchor must be 32 bytes/); + }); + }); + + // === Transaction ID Tests === + + describe('computeTxId', function () { + it('should compute base58(keccak256(signature))', function () { + const fakeSignature = new Uint8Array(65).fill(0xab); + const txId = IrysCommitmentTransactionBuilder.computeTxId(fakeSignature); + txId.should.be.a.String(); + txId.length.should.be.greaterThan(0); + }); + + it('should produce deterministic output', function () { + const sig = new Uint8Array(65).fill(0xcd); + const id1 = IrysCommitmentTransactionBuilder.computeTxId(sig); + const id2 = IrysCommitmentTransactionBuilder.computeTxId(sig); + id1.should.equal(id2); + }); + + it('should reject non-65-byte signatures', function () { + (() => IrysCommitmentTransactionBuilder.computeTxId(new Uint8Array(64))).should.throw( + /Signature must be 65 bytes/ + ); + }); + }); + + // === Broadcast Payload Tests === + + describe('createBroadcastPayload', function () { + it('should create valid JSON payload for STAKE', function () { + const fields = { + version: COMMITMENT_TX_VERSION, + anchor: testAnchor, + signer: testSigner, + commitmentType: { type: CommitmentTypeId.STAKE as const }, + chainId: testChainId, + fee: testFee, + value: testValue, + }; + const signature = new Uint8Array(65).fill(0xab); + + const payload = IrysCommitmentTransactionBuilder.createBroadcastPayload(fields, signature); + + payload.version.should.equal(2); + payload.anchor.should.be.a.String(); + payload.signer.should.be.a.String(); + should.deepEqual(payload.commitmentType, { type: 'stake' }); + payload.chainId.should.equal('1270'); + payload.fee.should.equal('1000'); + payload.value.should.equal('5000'); + payload.id.should.be.a.String(); + payload.signature.should.be.a.String(); + }); + + it('should create valid JSON payload for PLEDGE with pledgeCountBeforeExecuting', function () { + const fields = { + version: COMMITMENT_TX_VERSION, + anchor: testAnchor, + signer: testSigner, + commitmentType: { type: CommitmentTypeId.PLEDGE as const, pledgeCount: 42n }, + chainId: testChainId, + fee: testFee, + value: testValue, + }; + const signature = new Uint8Array(65).fill(0xcd); + + const payload = IrysCommitmentTransactionBuilder.createBroadcastPayload(fields, signature); + + should.deepEqual(payload.commitmentType, { type: 'pledge', pledgeCountBeforeExecuting: '42' }); + }); + }); + + // === Known-Good Test Vectors (from successful testnet transactions) === + // + // These vectors were captured from actual STAKE and PLEDGE transactions + // submitted to the Irys testnet using coins-sandbox/eth/irys/stake.ts. + // They verify our RLP encoding + prehash match the protocol exactly. + + describe('known-good test vectors', function () { + const testnetSigner = '0x22f9C9f1845D9b6C22b96Ef35E46E265aC4Af30c'; + const testnetSignerBytes = Uint8Array.from(Buffer.from(testnetSigner.slice(2), 'hex')); + const testnetChainId = 1270n; + + it('should match known STAKE RLP encoding and prehash', async function () { + // From stake_pledge.txt - successful STAKE transaction + // TX ID: 4XhUTrkhxr1RmUQbXUVRwbNZ6pKEYrAVo5ymdMY41fS5 + const anchorBase58 = '8JR2rD5DejnM2NuVSqqGa68dfye6ZKruT9rdh2Cn4B8y'; + const anchorBytes = decodeBase58(anchorBase58); + + const stakeBuilder = new IrysCommitmentTransactionBuilder(testApiUrl, testnetChainId); + + stakeBuilder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(100n) + .setValue(20000000000000000000000n) // 20000 IRYS + .setSigner(testnetSignerBytes) + .setAnchor(anchorBytes); + + const result = await stakeBuilder.build(); + + const expectedRlp = + '0xf84702a06c77daebc2db4e572e4f296983d1413fc10d4852e0fabfdb8323c9c69a2b85' + + '9e9422f9c9f1845d9b6c22b96ef35e46e265ac4af30c018204f6648a043c33c1937564800000'; + const actualRlp = '0x' + Buffer.from(result.rlpEncoded).toString('hex'); + actualRlp.should.equal(expectedRlp); + + const expectedPrehash = '0xe6fe57810c12785e3ce5fa64e2eb4da120b89ec0e469213715916abf36358d01'; + const actualPrehash = '0x' + Buffer.from(result.prehash).toString('hex'); + actualPrehash.should.equal(expectedPrehash); + }); + + it('should match known PLEDGE RLP encoding and prehash', async function () { + // From stake_pledge.txt - successful PLEDGE transaction + // TX ID: EsdiesC58S8eeY1SHM5jTfy84zYxFMUdKF89Ytr6PyNb + const anchorBase58 = 'jUShJPUACW4bxUSvZji65Q96MaqKDh7AFFALKnkapBn'; + const anchorBytes = decodeBase58(anchorBase58); + + const pledgeBuilder = new IrysCommitmentTransactionBuilder(testApiUrl, testnetChainId); + + pledgeBuilder + .setCommitmentType({ type: CommitmentTypeId.PLEDGE, pledgeCount: 0n }) + .setFee(100n) + .setValue(950000000000000000000n) // 950 IRYS + .setSigner(testnetSignerBytes) + .setAnchor(anchorBytes); + + const result = await pledgeBuilder.build(); + + const expectedRlp = + '0xf84802a00ae16c8476bbde2f28b2e4629d393dfe6fa7affcf0a0c4654f8246a9ba78970594' + + '22f9c9f1845d9b6c22b96ef35e46e265ac4af30cc202808204f66489337fe5feaf2d180000'; + const actualRlp = '0x' + Buffer.from(result.rlpEncoded).toString('hex'); + actualRlp.should.equal(expectedRlp); + + const expectedPrehash = '0xfe07c2f3c6e50d9c9e2cff57f6d7015b4528f425b6132f567e26bba745228102'; + const actualPrehash = '0x' + Buffer.from(result.prehash).toString('hex'); + actualPrehash.should.equal(expectedPrehash); + }); + }); + + // === Edge Case Tests === + + describe('edge cases', function () { + it('should handle zero fee and value', async function () { + builder + .setCommitmentType({ type: CommitmentTypeId.STAKE }) + .setFee(0n) + .setValue(0n) + .setSigner(testSigner) + .setAnchor(testAnchor); + + const result = await builder.build(); + result.prehash.length.should.equal(32); + }); + }); + + // === Anchor Fetch Tests === + + describe('fetchAnchor', function () { + it('should fetch and decode base58 anchor from API', async function () { + const mockAnchorBase58 = encodeBase58(testAnchor); + const scope = nock('https://testnet-node1.irys.xyz') + .get('/v1/anchor') + .reply(200, { blockHash: mockAnchorBase58 }); + + const anchor = await builder.fetchAnchor(); + anchor.should.be.instanceOf(Uint8Array); + anchor.length.should.equal(32); + Buffer.from(anchor).equals(Buffer.from(testAnchor)).should.be.true(); + scope.done(); + }); + + it('should throw on non-200 response', async function () { + const scope = nock('https://testnet-node1.irys.xyz').get('/v1/anchor').reply(500, 'Internal Server Error'); + + await builder.fetchAnchor().should.be.rejectedWith(/Internal Server Error/); + scope.done(); + }); + + it('should throw if anchor decodes to wrong length', async function () { + const shortAnchor = encodeBase58(new Uint8Array(16)); // 16 bytes instead of 32 + const scope = nock('https://testnet-node1.irys.xyz').get('/v1/anchor').reply(200, { blockHash: shortAnchor }); + + await builder.fetchAnchor().should.be.rejectedWith(/Expected 32 bytes/); + scope.done(); + }); + }); +}); diff --git a/modules/sdk-coin-irys/test/unit/irys.ts b/modules/sdk-coin-irys/test/unit/irys.ts new file mode 100644 index 0000000000..a136e579e7 --- /dev/null +++ b/modules/sdk-coin-irys/test/unit/irys.ts @@ -0,0 +1,90 @@ +import should from 'should'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Irys, TIrys } from '../../src'; + +describe('Irys', function () { + let bitgo: TestBitGoAPI; + let irys: Irys; + let tirys: TIrys; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('irys', Irys.createInstance); + bitgo.safeRegister('tirys', TIrys.createInstance); + bitgo.initializeTestVars(); + irys = bitgo.coin('irys') as Irys; + tirys = bitgo.coin('tirys') as TIrys; + }); + + describe('Irys Mainnet', function () { + it('should instantiate the coin', function () { + irys.should.be.instanceOf(Irys); + }); + + it('should have correct chain name', function () { + irys.getChain().should.equal('irys'); + }); + + it('should have correct full name', function () { + irys.getFullName().should.equal('Irys'); + }); + + it('should support TSS', function () { + irys.supportsTss().should.be.true(); + }); + + it('should use ECDSA MPC algorithm', function () { + irys.getMPCAlgorithm().should.equal('ecdsa'); + }); + + it('should return a commitment transaction builder', function () { + const builder = irys.getCommitmentTransactionBuilder(); + should.exist(builder); + builder.should.be.instanceOf(Object); + }); + + it('should return the Irys API URL', function () { + const apiUrl = irys.getIrysApiUrl(); + should.exist(apiUrl); + apiUrl!.should.be.a.String(); + apiUrl!.should.equal('https://node1.irys.xyz/v1'); + }); + }); + + describe('Irys Testnet (TIrys)', function () { + it('should instantiate the coin', function () { + tirys.should.be.instanceOf(TIrys); + tirys.should.be.instanceOf(Irys); + }); + + it('should have correct chain name', function () { + tirys.getChain().should.equal('tirys'); + }); + + it('should have correct full name', function () { + tirys.getFullName().should.equal('Irys Testnet'); + }); + + it('should support TSS', function () { + tirys.supportsTss().should.be.true(); + }); + + it('should use ECDSA MPC algorithm', function () { + tirys.getMPCAlgorithm().should.equal('ecdsa'); + }); + + it('should return a commitment transaction builder', function () { + const builder = tirys.getCommitmentTransactionBuilder(); + should.exist(builder); + builder.should.be.instanceOf(Object); + }); + + it('should return the Irys testnet API URL', function () { + const apiUrl = tirys.getIrysApiUrl(); + should.exist(apiUrl); + apiUrl!.should.be.a.String(); + apiUrl!.should.equal('https://testnet-node1.irys.xyz/v1'); + }); + }); +}); diff --git a/modules/sdk-coin-irys/tsconfig.json b/modules/sdk-coin-irys/tsconfig.json new file mode 100644 index 0000000000..7348748426 --- /dev/null +++ b/modules/sdk-coin-irys/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "strictPropertyInitialization": false, + "esModuleInterop": true, + "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules"], + "references": [ + { "path": "../abstract-eth" }, + { "path": "../sdk-core" }, + { "path": "../statics" }, + { "path": "../sdk-api" }, + { "path": "../sdk-test" } + ] +} diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index c27c55d513..d749686374 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -1887,8 +1887,6 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, - CoinFeature.SHARED_EVM_SIGNING, - CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, @@ -1905,12 +1903,11 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, - CoinFeature.SHARED_EVM_SIGNING, - CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.STAKING, ] ), account( diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 267b8f23a8..c203a65868 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -177,6 +177,8 @@ export interface EthereumNetwork extends AccountNetwork { readonly walletV4ImplementationAddress?: string; readonly nativeCoinOperationHashPrefix?: string; readonly tokenOperationHashPrefix?: string; + // Irys native API URL for commitment transactions (anchor, price, broadcast) + readonly irysApiUrl?: string; } export interface TronNetwork extends AccountNetwork { @@ -1839,6 +1841,7 @@ class Irys extends Mainnet implements EthereumNetwork { accountExplorerUrl = 'https://evm-explorer.irys.xyz/address/'; chainId = 3282; nativeCoinOperationHashPrefix = '3282'; + irysApiUrl = 'https://node1.irys.xyz/v1'; } class IrysTestnet extends Testnet implements EthereumNetwork { @@ -1853,6 +1856,7 @@ class IrysTestnet extends Testnet implements EthereumNetwork { forwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + irysApiUrl = 'https://testnet-node1.irys.xyz/v1'; } class Og extends Mainnet implements EthereumNetwork {