From dd32f5f2fba45e0a1fc1a9f2b0cf1425058fc5a6 Mon Sep 17 00:00:00 2001 From: Gautam2305 Date: Mon, 12 Jan 2026 15:44:57 +0530 Subject: [PATCH] feat(sdk-coin-xdc): add uploadKyc tx builder Ticket: SC-4804 --- .../sdk-coin-xdc/src/lib/XDCValidatorABI.json | 431 ++++++++++++++++++ modules/sdk-coin-xdc/src/lib/index.ts | 2 + .../src/lib/transactionBuilder.ts | 65 ++- .../sdk-coin-xdc/src/lib/uploadKycBuilder.ts | 128 ++++++ .../sdk-coin-xdc/src/lib/validatorContract.ts | 63 +++ .../test/unit/transactionBuilder/uploadKyc.ts | 274 +++++++++++ modules/sdk-coin-xdc/tsconfig.json | 3 +- 7 files changed, 961 insertions(+), 5 deletions(-) create mode 100644 modules/sdk-coin-xdc/src/lib/XDCValidatorABI.json create mode 100644 modules/sdk-coin-xdc/src/lib/uploadKycBuilder.ts create mode 100644 modules/sdk-coin-xdc/src/lib/validatorContract.ts create mode 100644 modules/sdk-coin-xdc/test/unit/transactionBuilder/uploadKyc.ts diff --git a/modules/sdk-coin-xdc/src/lib/XDCValidatorABI.json b/modules/sdk-coin-xdc/src/lib/XDCValidatorABI.json new file mode 100644 index 0000000000..0b078826e1 --- /dev/null +++ b/modules/sdk-coin-xdc/src/lib/XDCValidatorABI.json @@ -0,0 +1,431 @@ +[ + { + "constant": false, + "inputs": [ + { + "name": "kychash", + "type": "string" + } + ], + "name": "uploadKYC", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "propose", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "vote", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_candidate", + "type": "address" + }, + { + "name": "_cap", + "type": "uint256" + } + ], + "name": "unvote", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "resign", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_blockNumber", + "type": "uint256" + }, + { + "name": "_index", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCandidates", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "getCandidateCap", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "getCandidateOwner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_candidate", + "type": "address" + }, + { + "name": "_voter", + "type": "address" + } + ], + "name": "getVoterCap", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "getVoters", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_candidate", + "type": "address" + } + ], + "name": "isCandidate", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_address", + "type": "address" + } + ], + "name": "getLatestKYC", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_address", + "type": "address" + } + ], + "name": "getHashCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getOwnerCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_invalidCandidate", + "type": "address" + } + ], + "name": "voteInvalidKYC", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_invalidCandidate", + "type": "address" + } + ], + "name": "invalidPercent", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_voter", + "type": "address" + }, + { + "indexed": false, + "name": "_candidate", + "type": "address" + }, + { + "indexed": false, + "name": "_cap", + "type": "uint256" + } + ], + "name": "Vote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_voter", + "type": "address" + }, + { + "indexed": false, + "name": "_candidate", + "type": "address" + }, + { + "indexed": false, + "name": "_cap", + "type": "uint256" + } + ], + "name": "Unvote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "_candidate", + "type": "address" + }, + { + "indexed": false, + "name": "_cap", + "type": "uint256" + } + ], + "name": "Propose", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "_candidate", + "type": "address" + } + ], + "name": "Resign", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "_blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "name": "_cap", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "kycHash", + "type": "string" + } + ], + "name": "UploadedKYC", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_masternodeOwner", + "type": "address" + }, + { + "indexed": false, + "name": "_masternodes", + "type": "address[]" + } + ], + "name": "InvalidatedNode", + "type": "event" + } +] diff --git a/modules/sdk-coin-xdc/src/lib/index.ts b/modules/sdk-coin-xdc/src/lib/index.ts index 20f9e761a1..6740a21455 100644 --- a/modules/sdk-coin-xdc/src/lib/index.ts +++ b/modules/sdk-coin-xdc/src/lib/index.ts @@ -2,5 +2,7 @@ import * as Utils from './utils'; export { TransactionBuilder } from './transactionBuilder'; export { TransferBuilder } from './transferBuilder'; +export { UploadKycBuilder, UploadKycCall } from './uploadKycBuilder'; export { Transaction, KeyPair } from '@bitgo/abstract-eth'; export { Utils }; +export * from './validatorContract'; diff --git a/modules/sdk-coin-xdc/src/lib/transactionBuilder.ts b/modules/sdk-coin-xdc/src/lib/transactionBuilder.ts index 2e6d6ea8d2..367db02783 100644 --- a/modules/sdk-coin-xdc/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-xdc/src/lib/transactionBuilder.ts @@ -1,17 +1,20 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { TransactionBuilder as AbstractTransactionBuilder, Transaction } from '@bitgo/abstract-eth'; +import { BuildTransactionError, TransactionType, PublicKey } from '@bitgo/sdk-core'; +import { TransactionBuilder as AbstractTransactionBuilder, Transaction, TxData } from '@bitgo/abstract-eth'; import { getCommon } from './utils'; import { TransferBuilder } from './transferBuilder'; +import { UploadKycBuilder, UploadKycCall } from './uploadKycBuilder'; export class TransactionBuilder extends AbstractTransactionBuilder { protected _transfer: TransferBuilder; - private _signatures: any; + private _signatures: { publicKey: PublicKey; signature: Buffer }[]; + private _uploadKycBuilder?: UploadKycBuilder; constructor(_coinConfig: Readonly) { super(_coinConfig); this._common = getCommon(this._coinConfig.network.type); this.transaction = new Transaction(this._coinConfig, this._common); + this._signatures = []; } /** @inheritdoc */ @@ -25,7 +28,61 @@ export class TransactionBuilder extends AbstractTransactionBuilder { return this._transfer; } - addSignature(publicKey, signature) { + /** + * Gets the uploadKYC builder if it exists, or creates a new one for this transaction + * + * @returns {UploadKycBuilder} the uploadKYC builder + * @throws {BuildTransactionError} if transaction type is not ContractCall + */ + uploadKyc(): UploadKycBuilder { + if (this._type !== TransactionType.ContractCall) { + throw new BuildTransactionError('uploadKYC can only be set for contract call transactions'); + } + if (!this._uploadKycBuilder) { + this._uploadKycBuilder = new UploadKycBuilder(this._coinConfig); + } + return this._uploadKycBuilder; + } + + /** @inheritdoc */ + async build(): Promise { + // If uploadKYC builder is set, prepare the contract call data before validation + if (this._uploadKycBuilder && this._type === TransactionType.ContractCall) { + const uploadKycCall: UploadKycCall = this._uploadKycBuilder.build(); + this.contract(uploadKycCall.contractAddress); + this.data(uploadKycCall.serialize()); + } + + return (await super.build()) as Transaction; + } + + /** + * Build the transaction data for uploadKYC contract call + * @private + */ + private buildUploadKycTransaction(): TxData { + if (!this._uploadKycBuilder) { + throw new BuildTransactionError('uploadKYC builder not initialized'); + } + + const uploadKycCall: UploadKycCall = this._uploadKycBuilder.build(); + const encodedData = uploadKycCall.serialize(); + + return this.buildBase(encodedData); + } + + /** @inheritdoc */ + protected getTransactionData(): TxData { + // If uploadKYC builder is set, build uploadKYC transaction + if (this._uploadKycBuilder && this._type === TransactionType.ContractCall) { + return this.buildUploadKycTransaction(); + } + + // Otherwise, use the parent implementation + return super.getTransactionData(); + } + + addSignature(publicKey: PublicKey, signature: Buffer): void { this._signatures = []; this._signatures.push({ publicKey, signature }); } diff --git a/modules/sdk-coin-xdc/src/lib/uploadKycBuilder.ts b/modules/sdk-coin-xdc/src/lib/uploadKycBuilder.ts new file mode 100644 index 0000000000..904d52cb8b --- /dev/null +++ b/modules/sdk-coin-xdc/src/lib/uploadKycBuilder.ts @@ -0,0 +1,128 @@ +/** + * @prettier + */ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { BuildTransactionError, InvalidParameterValueError } from '@bitgo/sdk-core'; +import { ContractCall } from '@bitgo/abstract-eth'; +import { UPLOAD_KYC_METHOD_ID, getValidatorContractAddress } from './validatorContract'; + +/** + * Represents an XDC uploadKYC contract call + * This is used to submit KYC document hashes (IPFS hashes) to the XDC Validator contract + */ +export class UploadKycCall extends ContractCall { + public readonly contractAddress: string; + public readonly ipfsHash: string; + + constructor(contractAddress: string, ipfsHash: string) { + // uploadKYC(string) - takes a single string parameter (the IPFS hash) + super(UPLOAD_KYC_METHOD_ID, ['string'], [ipfsHash]); + this.contractAddress = contractAddress; + this.ipfsHash = ipfsHash; + } +} + +/** + * Builder for XDC uploadKYC transactions + * + * This builder creates transactions that upload KYC document hashes to the XDC Validator contract. + * The flow is: + * 1. Upload KYC document to IPFS → get IPFS hash (e.g., "Qm...") + * 2. Use this builder to create a transaction that submits the hash to the validator contract + * 3. The transaction is signed using TSS/MPC + * 4. After successful upload, the address can propose a validator candidate + * + * @example + * ```typescript + * const builder = new UploadKycBuilder(coinConfig); + * const call = builder + * .ipfsHash('QmRealIPFSHash...') + * .build(); + * ``` + */ +export class UploadKycBuilder { + private _ipfsHash?: string; + private _contractAddress?: string; + private readonly _coinConfig: Readonly; + + constructor(coinConfig: Readonly) { + this._coinConfig = coinConfig; + } + + /** + * Set the IPFS hash of the uploaded KYC document + * + * @param {string} hash - The IPFS hash (e.g., "QmRealIPFSHash...") + * @returns {UploadKycBuilder} this builder instance + * @throws {InvalidParameterValueError} if the hash is invalid + */ + ipfsHash(hash: string): this { + if (!hash || hash.trim().length === 0) { + throw new InvalidParameterValueError('IPFS hash cannot be empty'); + } + + // Basic IPFS hash validation (should start with 'Qm' for v0 or 'b' for v1) + if (!hash.startsWith('Qm') && !hash.startsWith('b')) { + throw new InvalidParameterValueError( + 'Invalid IPFS hash format. Expected hash starting with "Qm" (v0) or "b" (v1)' + ); + } + + this._ipfsHash = hash; + return this; + } + + /** + * Set a custom validator contract address + * If not set, the default address for the network will be used + * + * @param {string} address - The validator contract address + * @returns {UploadKycBuilder} this builder instance + * @throws {InvalidParameterValueError} if the address is invalid + */ + contractAddress(address: string): this { + if (!address || address.trim().length === 0) { + throw new InvalidParameterValueError('Contract address cannot be empty'); + } + + // Basic Ethereum address validation + if (!/^(0x)?[0-9a-fA-F]{40}$/.test(address)) { + throw new InvalidParameterValueError('Invalid contract address format'); + } + + this._contractAddress = address.toLowerCase().startsWith('0x') ? address : `0x${address}`; + return this; + } + + /** + * Build the uploadKYC contract call + * + * @returns {UploadKycCall} the constructed contract call + * @throws {BuildTransactionError} if required fields are missing + */ + build(): UploadKycCall { + this.validateMandatoryFields(); + const contractAddress = this._contractAddress || this.getDefaultContractAddress(); + const ipfsHash = this._ipfsHash as string; // validated by validateMandatoryFields + return new UploadKycCall(contractAddress, ipfsHash); + } + + /** + * Validate that all mandatory fields are set + * @private + */ + private validateMandatoryFields(): void { + if (!this._ipfsHash) { + throw new BuildTransactionError('Missing IPFS hash for uploadKYC transaction'); + } + } + + /** + * Get the default validator contract address for the current network + * @private + */ + private getDefaultContractAddress(): string { + const isTestnet = this._coinConfig.name === 'txdc'; + return getValidatorContractAddress(isTestnet); + } +} diff --git a/modules/sdk-coin-xdc/src/lib/validatorContract.ts b/modules/sdk-coin-xdc/src/lib/validatorContract.ts new file mode 100644 index 0000000000..44e4f18755 --- /dev/null +++ b/modules/sdk-coin-xdc/src/lib/validatorContract.ts @@ -0,0 +1,63 @@ +/** + * XDC Validator Contract Constants and ABI + * + * This file contains the contract address and ABI for the XDC Validator contract + * which handles KYC uploads and validator staking operations. + * + * Contract Address: 0x0000000000000000000000000000000000000088 (System Contract) + * Source: XDC Network Validator Contract + * Reference: https://github.com/XinFinOrg/XDPoSChain + */ + +import XDCValidatorABI from './XDCValidatorABI.json'; + +/** + * XDC Validator Contract Address (Testnet) + * This is the deployed contract address on XDC Apothem testnet (chainId: 51) + */ +export const XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET = '0x0000000000000000000000000000000000000088'; + +/** + * XDC Validator Contract Address (Mainnet) + * This is the deployed contract address on XDC mainnet (chainId: 50) + */ +export const XDC_VALIDATOR_CONTRACT_ADDRESS_MAINNET = '0x0000000000000000000000000000000000000088'; + +/** + * uploadKYC method ID + * keccak256("uploadKYC(string)") = 0x8c60a2c3 + */ +export const UPLOAD_KYC_METHOD_ID = '0x8c60a2c3'; + +/** + * propose method ID + * keccak256("propose(address)") = 0xda35c664 + */ +export const PROPOSE_METHOD_ID = '0xda35c664'; + +/** + * Full XDC Validator Contract ABI + * Imported from JSON file for easy use with Web3/XDC3 + */ +export const XDC_VALIDATOR_ABI = XDCValidatorABI; + +/** + * Get the uploadKYC function ABI + * Useful for encoding function calls + */ +export const UPLOAD_KYC_ABI = XDCValidatorABI.find((item) => item.name === 'uploadKYC'); + +/** + * Get the propose function ABI + * Useful for encoding function calls + */ +export const PROPOSE_ABI = XDCValidatorABI.find((item) => item.name === 'propose'); + +/** + * Get the validator contract address for the given network + * @param isTestnet - whether to use testnet or mainnet + * @returns the validator contract address + */ +export function getValidatorContractAddress(isTestnet: boolean): string { + return isTestnet ? XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET : XDC_VALIDATOR_CONTRACT_ADDRESS_MAINNET; +} diff --git a/modules/sdk-coin-xdc/test/unit/transactionBuilder/uploadKyc.ts b/modules/sdk-coin-xdc/test/unit/transactionBuilder/uploadKyc.ts new file mode 100644 index 0000000000..eeaaecedd2 --- /dev/null +++ b/modules/sdk-coin-xdc/test/unit/transactionBuilder/uploadKyc.ts @@ -0,0 +1,274 @@ +import { getBuilder } from '../getBuilder'; +import should from 'should'; +import { TransactionType } from '@bitgo/sdk-core'; +import { coins } from '@bitgo/statics'; +import { + UploadKycBuilder, + UploadKycCall, + XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET, + UPLOAD_KYC_METHOD_ID, +} from '../../../src/lib'; + +describe('XDC Upload KYC Builder', () => { + const coinConfig = coins.get('txdc'); + + describe('UploadKycBuilder', () => { + it('should build uploadKYC call with valid IPFS hash', () => { + const builder = new UploadKycBuilder(coinConfig); + const ipfsHash = 'QmRealIPFSHashExample123456789012345678901234'; + + const call = builder.ipfsHash(ipfsHash).build(); + + should.exist(call); + call.should.be.instanceOf(UploadKycCall); + call.ipfsHash.should.equal(ipfsHash); + call.contractAddress.should.equal(XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET); + }); + + it('should build uploadKYC call with custom contract address', () => { + const builder = new UploadKycBuilder(coinConfig); + const ipfsHash = 'QmCustomIPFSHash123456789012345678901234567'; + const customAddress = '0x1234567890123456789012345678901234567890'; + + const call = builder.ipfsHash(ipfsHash).contractAddress(customAddress).build(); + + should.exist(call); + call.contractAddress.should.equal(customAddress); + }); + + it('should accept IPFS v1 hash format (starting with "b")', () => { + const builder = new UploadKycBuilder(coinConfig); + const ipfsHash = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'; + + const call = builder.ipfsHash(ipfsHash).build(); + + should.exist(call); + call.ipfsHash.should.equal(ipfsHash); + }); + + it('should throw error when IPFS hash is empty', () => { + const builder = new UploadKycBuilder(coinConfig); + + (() => builder.ipfsHash('')).should.throw('IPFS hash cannot be empty'); + }); + + it('should throw error when IPFS hash format is invalid', () => { + const builder = new UploadKycBuilder(coinConfig); + + (() => builder.ipfsHash('InvalidHash123')).should.throw( + 'Invalid IPFS hash format. Expected hash starting with "Qm" (v0) or "b" (v1)' + ); + }); + + it('should throw error when contract address is empty', () => { + const builder = new UploadKycBuilder(coinConfig); + + (() => builder.contractAddress('')).should.throw('Contract address cannot be empty'); + }); + + it('should throw error when contract address format is invalid', () => { + const builder = new UploadKycBuilder(coinConfig); + + (() => builder.contractAddress('InvalidAddress')).should.throw('Invalid contract address format'); + }); + + it('should throw error when building without IPFS hash', () => { + const builder = new UploadKycBuilder(coinConfig); + + (() => builder.build()).should.throw('Missing IPFS hash for uploadKYC transaction'); + }); + + it('should normalize contract address with 0x prefix', () => { + const builder = new UploadKycBuilder(coinConfig); + const ipfsHash = 'QmTestHash123456789012345678901234567890123'; + const addressWithoutPrefix = '1234567890123456789012345678901234567890'; + + const call = builder.ipfsHash(ipfsHash).contractAddress(addressWithoutPrefix).build(); + + call.contractAddress.should.equal('0x' + addressWithoutPrefix); + }); + }); + + describe('UploadKycCall', () => { + it('should serialize uploadKYC call correctly', () => { + const ipfsHash = 'QmTestIPFSHash1234567890123456789012345678'; + const contractAddress = XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET; + + const call = new UploadKycCall(contractAddress, ipfsHash); + const serialized = call.serialize(); + + should.exist(serialized); + serialized.should.be.type('string'); + // Should start with the method ID + serialized.should.startWith(UPLOAD_KYC_METHOD_ID); + }); + + it('should have correct properties', () => { + const ipfsHash = 'QmTestIPFSHash1234567890123456789012345678'; + const contractAddress = '0x0000000000000000000000000000000000000088'; + + const call = new UploadKycCall(contractAddress, ipfsHash); + + call.contractAddress.should.equal(contractAddress); + call.ipfsHash.should.equal(ipfsHash); + }); + }); + + describe('TransactionBuilder integration', () => { + it('should build uploadKYC transaction with TransactionBuilder', async () => { + const txBuilder = getBuilder('txdc'); + const ipfsHash = 'QmTestIPFSHash1234567890123456789012345678'; + + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '10000000000', + gasLimit: '100000', + }); + txBuilder.counter(1); + txBuilder.uploadKyc().ipfsHash(ipfsHash); + + const tx = await txBuilder.build(); + + should.exist(tx); + const txJson = tx.toJson(); + should.exist(txJson.to); + txJson.to!.should.equal(XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET); + txJson.value.should.equal('0'); + should.exist(txJson.data); + // Data should start with uploadKYC method ID + txJson.data.should.startWith(UPLOAD_KYC_METHOD_ID); + }); + + it('should throw error when uploadKyc() is called on non-ContractCall transaction', () => { + const txBuilder = getBuilder('txdc'); + + txBuilder.type(TransactionType.Send); + + (() => txBuilder.uploadKyc()).should.throw('uploadKYC can only be set for contract call transactions'); + }); + + it('should build transaction with custom validator contract address', async () => { + const txBuilder = getBuilder('txdc'); + const ipfsHash = 'QmCustomIPFSHash123456789012345678901234567'; + const customAddress = '0x1234567890123456789012345678901234567890'; + + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '10000000000', + gasLimit: '100000', + }); + txBuilder.counter(1); + txBuilder.uploadKyc().ipfsHash(ipfsHash).contractAddress(customAddress); + + const tx = await txBuilder.build(); + + should.exist(tx); + const txJson = tx.toJson(); + should.exist(txJson.to); + txJson.to!.should.equal(customAddress); + }); + + it('should build and serialize uploadKYC transaction', async () => { + const txBuilder = getBuilder('txdc'); + const ipfsHash = 'QmRealIPFSHashForSerialization1234567890123'; + + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '10000000000', + gasLimit: '100000', + }); + txBuilder.counter(5); + txBuilder.uploadKyc().ipfsHash(ipfsHash); + + const tx = await txBuilder.build(); + const serialized = tx.toBroadcastFormat(); + + should.exist(serialized); + serialized.should.be.type('string'); + // Should be a valid hex string + serialized.should.match(/^(0x)?[0-9a-f]+$/i); + }); + + it('should parse uploadKYC transaction from hex', async () => { + const txBuilder = getBuilder('txdc'); + const ipfsHash = 'QmParseTestHash12345678901234567890123456'; + + // First build a transaction + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '10000000000', + gasLimit: '100000', + }); + txBuilder.counter(10); + txBuilder.uploadKyc().ipfsHash(ipfsHash); + + const tx = await txBuilder.build(); + const serialized = tx.toBroadcastFormat(); + + // Now parse it back + const txBuilder2 = getBuilder('txdc'); + txBuilder2.from(serialized); + const parsedTx = await txBuilder2.build(); + + should.exist(parsedTx); + const parsedJson = parsedTx.toJson(); + should.exist(parsedJson.to); + parsedJson.to!.should.equal(XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET); + parsedJson.nonce.should.equal(10); + }); + }); + + describe('Real-world scenarios', () => { + it('should create transaction matching sandbox code pattern', async () => { + const txBuilder = getBuilder('txdc'); + // Mock IPFS hash similar to what would be generated + const mockIPFSHash = 'Qm' + 'a'.repeat(44); + + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '20000000000', + gasLimit: '200000', + }); + txBuilder.counter(0); + txBuilder.uploadKyc().ipfsHash(mockIPFSHash); + + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + + // Verify transaction structure matches expected format + should.exist(txJson.to); + txJson.to!.should.equal(XDC_VALIDATOR_CONTRACT_ADDRESS_TESTNET); + txJson.value.should.equal('0'); + should.exist(txJson.data); + txJson.data.should.startWith(UPLOAD_KYC_METHOD_ID); + should.exist(txJson.gasLimit); + should.exist(txJson.nonce); + }); + + it('should handle multiple IPFS hash formats', async () => { + const testHashes = [ + 'QmRealIPFSHashExample123456789012345678901234', // v0 + 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi', // v1 + 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG', // real example + ]; + + for (const hash of testHashes) { + const txBuilder = getBuilder('txdc'); + + txBuilder.type(TransactionType.ContractCall); + txBuilder.fee({ + fee: '10000000000', + gasLimit: '100000', + }); + txBuilder.counter(1); + txBuilder.uploadKyc().ipfsHash(hash); + + const tx = await txBuilder.build(); + should.exist(tx); + + const txJson = tx.toJson(); + txJson.data.should.startWith(UPLOAD_KYC_METHOD_ID); + } + }); + }); +}); diff --git a/modules/sdk-coin-xdc/tsconfig.json b/modules/sdk-coin-xdc/tsconfig.json index 52c51f35c6..915cdd540a 100644 --- a/modules/sdk-coin-xdc/tsconfig.json +++ b/modules/sdk-coin-xdc/tsconfig.json @@ -5,9 +5,10 @@ "rootDir": "./", "strictPropertyInitialization": false, "esModuleInterop": true, + "resolveJsonModule": true, "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] }, - "include": ["src/**/*", "test/**/*"], + "include": ["src/**/*", "test/**/*", "src/**/*.json"], "exclude": ["node_modules"], "references": [ {