diff --git a/packages/ripple-binary-codec/src/types/xchain-bridge.ts b/packages/ripple-binary-codec/src/types/xchain-bridge.ts index ec80027c..3e533764 100644 --- a/packages/ripple-binary-codec/src/types/xchain-bridge.ts +++ b/packages/ripple-binary-codec/src/types/xchain-bridge.ts @@ -67,20 +67,20 @@ class XChainBridge extends SerializedType { return value } - if (isXChainBridgeObject(value)) { - const bytes: Array = [] - this.TYPE_ORDER.forEach((item) => { - const { name, type } = item - if (type === AccountID) { - bytes.push(Buffer.from([0x14])) - } - const object = type.from(value[name]) - bytes.push(object.toBytes()) - }) - return new XChainBridge(Buffer.concat(bytes)) + if (!isXChainBridgeObject(value)) { + throw new Error('Invalid type to construct an XChainBridge') } - throw new Error('Invalid type to construct an XChainBridge') + const bytes: Array = [] + this.TYPE_ORDER.forEach((item) => { + const { name, type } = item + if (type === AccountID) { + bytes.push(Buffer.from([0x14])) + } + const object = type.from(value[name]) + bytes.push(object.toBytes()) + }) + return new XChainBridge(Buffer.concat(bytes)) } /** diff --git a/packages/xrpl/src/models/ledger/Bridge.ts b/packages/xrpl/src/models/ledger/Bridge.ts index 85e63c9a..5b773200 100644 --- a/packages/xrpl/src/models/ledger/Bridge.ts +++ b/packages/xrpl/src/models/ledger/Bridge.ts @@ -22,13 +22,6 @@ export default interface Bridge extends BaseLedgerEntry, HasPreviousTxnID { */ SignatureReward: Amount - /** - * The minimum amount, in XRP, required for an {@link XChainAccountCreateCommit} - * transaction. If this isn't present, the {@link XChainAccountCreateCommit} - * transaction will fail. This field can only be present on XRP-XRP bridges. - */ - MinAccountCreateAmount?: string - /** * The door accounts and assets of the bridge this object correlates to. */ @@ -58,6 +51,13 @@ export default interface Bridge extends BaseLedgerEntry, HasPreviousTxnID { */ XChainAccountClaimCount: string + /** + * The minimum amount, in XRP, required for an {@link XChainAccountCreateCommit} + * transaction. If this isn't present, the {@link XChainAccountCreateCommit} + * transaction will fail. This field can only be present on XRP-XRP bridges. + */ + MinAccountCreateAmount?: string + /** * A bit-map of boolean flags. No flags are defined for Bridges, so this value * is always 0. diff --git a/packages/xrpl/src/models/transactions/XChainAccountCreateCommit.ts b/packages/xrpl/src/models/transactions/XChainAccountCreateCommit.ts index d50b967d..167a2035 100644 --- a/packages/xrpl/src/models/transactions/XChainAccountCreateCommit.ts +++ b/packages/xrpl/src/models/transactions/XChainAccountCreateCommit.ts @@ -14,7 +14,7 @@ import { * The XChainAccountCreateCommit transaction creates a new account on one of the * chains a bridge connects, which serves as the bridge entrance for that chain. * - * Warning: This transaction should only be executed if the witness attestations + * WARNING: This transaction should only be executed if the witness attestations * will be reliably delivered to the destination chain. If the signatures aren't * delivered, then account creation will be blocked until attestations are received. * This can be used maliciously; to disable this transaction on XRP-XRP bridges, diff --git a/packages/xrpl/test/integration/fundWallet.test.ts b/packages/xrpl/test/integration/fundWallet.test.ts index 173d4d19..de35e5a5 100644 --- a/packages/xrpl/test/integration/fundWallet.test.ts +++ b/packages/xrpl/test/integration/fundWallet.test.ts @@ -21,7 +21,7 @@ async function generate_faucet_wallet_and_fund_again( faucetPath, usageContext: 'integration-test', }) - assert.notEqual(wallet, undefined) + assert.notStrictEqual(wallet, undefined) assert(isValidClassicAddress(wallet.classicAddress)) assert(isValidXAddress(wallet.getXAddress())) @@ -103,7 +103,7 @@ describe('fundWallet', function () { usageContext: 'integration-test', }) - assert.notEqual(wallet, undefined) + assert.notStrictEqual(wallet, undefined) assert(isValidClassicAddress(wallet.classicAddress)) assert(isValidXAddress(wallet.getXAddress())) @@ -137,7 +137,7 @@ describe('fundWallet', function () { usageContext: 'integration-test', }) assert.equal(balance, '2000') - assert.notEqual(wallet, undefined) + assert.notStrictEqual(wallet, undefined) assert(isValidClassicAddress(wallet.classicAddress)) assert(isValidXAddress(wallet.getXAddress())) diff --git a/packages/xrpl/test/integration/index.ts b/packages/xrpl/test/integration/index.ts index de4b212d..bf2335a7 100644 --- a/packages/xrpl/test/integration/index.ts +++ b/packages/xrpl/test/integration/index.ts @@ -19,6 +19,14 @@ export * from './transactions/paymentChannelCreate.test' export * from './transactions/paymentChannelFund.test' export * from './transactions/signerListSet.test' export * from './transactions/trustSet.test' +export * from './transactions/xchainAccountCreateCommit.test' +export * from './transactions/xchainAddAccountCreateAttestation.test' +export * from './transactions/xchainAddClaimAttestation.test' +export * from './transactions/xchainClaim.test' +export * from './transactions/xchainCreateBridge.test' +export * from './transactions/xchainCreateClaimID.test' +export * from './transactions/xchainCommit.test' +export * from './transactions/xchainModifyBridge.test' // Requests export * from './requests/accountChannels.test' diff --git a/packages/xrpl/test/integration/setup.ts b/packages/xrpl/test/integration/setup.ts index 288b3cd5..2704d0c5 100644 --- a/packages/xrpl/test/integration/setup.ts +++ b/packages/xrpl/test/integration/setup.ts @@ -1,7 +1,26 @@ -import { Client, Wallet } from '../../src' +import { assert } from 'chai' + +import { + Client, + SignerListSet, + Wallet, + XChainBridge, + XChainCreateBridge, +} from '../../src' import serverUrl from './serverUrl' -import { fundAccount } from './utils' +import { + GENESIS_ACCOUNT, + fundAccount, + generateFundedWallet, + testTransaction, +} from './utils' + +interface TestBridge { + xchainBridge: XChainBridge + witness: Wallet + signatureReward: string +} export interface XrplIntegrationTestContext { client: Client @@ -32,15 +51,86 @@ async function connectWithRetry(client: Client, tries = 0): Promise { export async function setupClient( server = serverUrl, ): Promise { - const context: XrplIntegrationTestContext = { - client: new Client(server, { timeout: 200000 }), - wallet: Wallet.generate(), - } - return connectWithRetry(context.client).then(async () => { - await fundAccount(context.client, context.wallet, { + const client = new Client(server, { timeout: 200000 }) + const wallet = Wallet.generate() + return connectWithRetry(client).then(async () => { + await fundAccount(client, wallet, { count: 20, delayMs: 1000, }) + const context: XrplIntegrationTestContext = { + client, + wallet, + } return context }) } + +export async function setupBridge(client: Client): Promise { + const doorAccount = await generateFundedWallet(client) + const signatureReward = '200' + const xchainBridge: XChainBridge = { + LockingChainDoor: doorAccount.classicAddress, + LockingChainIssue: { currency: 'XRP' }, + IssuingChainDoor: GENESIS_ACCOUNT, + IssuingChainIssue: { currency: 'XRP' }, + } + const setupTx: XChainCreateBridge = { + TransactionType: 'XChainCreateBridge', + Account: doorAccount.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + MinAccountCreateAmount: '10000000', + } + + await testTransaction(client, setupTx, doorAccount) + + // confirm that the transaction actually went through + const accountObjectsResponse = await client.request({ + command: 'account_objects', + account: doorAccount.classicAddress, + type: 'bridge', + }) + assert.lengthOf( + accountObjectsResponse.result.account_objects, + 1, + 'Should be exactly one bridge owned by the account', + ) + + const witnessWallet = await generateFundedWallet(client) + + const signerTx: SignerListSet = { + TransactionType: 'SignerListSet', + Account: doorAccount.classicAddress, + SignerEntries: [ + { + SignerEntry: { + Account: witnessWallet.classicAddress, + SignerWeight: 1, + }, + }, + ], + SignerQuorum: 1, + } + await testTransaction(client, signerTx, doorAccount) + + const signerAccountInfoResponse = await client.request({ + command: 'account_info', + account: doorAccount.classicAddress, + signer_lists: true, + }) + const signerListInfo = + signerAccountInfoResponse.result.account_data.signer_lists?.[0] + assert.deepEqual( + signerListInfo?.SignerEntries, + signerTx.SignerEntries, + 'SignerEntries were not set properly', + ) + assert.equal( + signerListInfo?.SignerQuorum, + signerTx.SignerQuorum, + 'SignerQuorum was not set properly', + ) + + return { xchainBridge, witness: witnessWallet, signatureReward } +} diff --git a/packages/xrpl/test/integration/transactions/signerListSet.test.ts b/packages/xrpl/test/integration/transactions/signerListSet.test.ts index 1181e1eb..dc022b62 100644 --- a/packages/xrpl/test/integration/transactions/signerListSet.test.ts +++ b/packages/xrpl/test/integration/transactions/signerListSet.test.ts @@ -1,3 +1,5 @@ +import { assert } from 'chai' + import { SignerListSet } from '../../../src' import serverUrl from '../serverUrl' import { @@ -42,6 +44,24 @@ describe('SignerListSet', function () { SignerQuorum: 2, } await testTransaction(testContext.client, tx, testContext.wallet) + + const accountInfoResponse = await testContext.client.request({ + command: 'account_info', + account: testContext.wallet.classicAddress, + signer_lists: true, + }) + const signerListInfo = + accountInfoResponse.result.account_data.signer_lists?.[0] + assert.deepEqual( + signerListInfo?.SignerEntries, + tx.SignerEntries, + 'SignerEntries were not set properly', + ) + assert.equal( + signerListInfo?.SignerQuorum, + tx.SignerQuorum, + 'SignerQuorum was not set properly', + ) }, TIMEOUT, ) diff --git a/packages/xrpl/test/integration/transactions/xchainAccountCreateCommit.test.ts b/packages/xrpl/test/integration/transactions/xchainAccountCreateCommit.test.ts new file mode 100644 index 00000000..27f105ea --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainAccountCreateCommit.test.ts @@ -0,0 +1,64 @@ +import { assert } from 'chai' + +import { XChainAccountCreateCommit, Wallet } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, + setupBridge, +} from '../setup' +import { generateFundedWallet, getXRPBalance, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainAccountCreateCommit', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge, signatureReward } = await setupBridge( + testContext.client, + ) + const initialBalance = Number( + await getXRPBalance(testContext.client, xchainBridge.LockingChainDoor), + ) + + // actually test XChainAccountCreateCommit + const wallet2 = await generateFundedWallet(testContext.client) + const destination = Wallet.generate() + const amount = 10000000 + const tx: XChainAccountCreateCommit = { + TransactionType: 'XChainAccountCreateCommit', + Account: wallet2.classicAddress, + XChainBridge: xchainBridge, + Amount: amount.toString(), + SignatureReward: signatureReward, + Destination: destination.classicAddress, + } + + await testTransaction(testContext.client, tx, wallet2) + + const accountInfoResponse2 = await testContext.client.request({ + command: 'account_info', + account: xchainBridge.LockingChainDoor, + }) + const finalBalance = Number( + accountInfoResponse2.result.account_data.Balance, + ) + assert.equal( + finalBalance, + initialBalance + amount + Number(signatureReward), + "The bridge door's balance should go up by the amount committed", + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainAddAccountCreateAttestation.test.ts b/packages/xrpl/test/integration/transactions/xchainAddAccountCreateAttestation.test.ts new file mode 100644 index 00000000..1e72b61b --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainAddAccountCreateAttestation.test.ts @@ -0,0 +1,76 @@ +import { encode } from 'ripple-binary-codec' +import { sign } from 'ripple-keypairs' + +import { + Wallet, + XChainAddAccountCreateAttestation, + xrpToDrops, +} from '../../../src' +import serverUrl from '../serverUrl' +import { + setupBridge, + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateBridge', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge, witness, signatureReward } = await setupBridge( + testContext.client, + ) + const destination = Wallet.generate() + const otherChainSource = Wallet.generate() + + const attestationToSign = { + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: xrpToDrops(300), + AttestationRewardAccount: witness.classicAddress, + WasLockingChainSend: 0, + XChainAccountCreateCount: 1, + Destination: destination.classicAddress, + SignatureReward: signatureReward, + } + const encodedAttestation = encode(attestationToSign) + const attestationSignature = sign(encodedAttestation, witness.privateKey) + + const tx: XChainAddAccountCreateAttestation = { + TransactionType: 'XChainAddAccountCreateAttestation', + Account: testContext.wallet.classicAddress, + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: xrpToDrops(300), + WasLockingChainSend: 0, + XChainAccountCreateCount: 1, + Destination: destination.classicAddress, + SignatureReward: signatureReward, + PublicKey: witness.publicKey, + Signature: attestationSignature, + AttestationRewardAccount: witness.classicAddress, + AttestationSignerAccount: witness.classicAddress, + } + await testTransaction(testContext.client, tx, testContext.wallet) + + // should not throw + await testContext.client.request({ + command: 'account_info', + account: destination.classicAddress, + }) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainAddClaimAttestation.test.ts b/packages/xrpl/test/integration/transactions/xchainAddClaimAttestation.test.ts new file mode 100644 index 00000000..e653938a --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainAddClaimAttestation.test.ts @@ -0,0 +1,274 @@ +/* eslint-disable max-statements -- necessary because transfers require a lot of steps */ +import { assert } from 'chai' +import { encode } from 'ripple-binary-codec' +import { sign } from 'ripple-keypairs' + +import { + AccountSet, + AccountSetAsfFlags, + IssuedCurrency, + IssuedCurrencyAmount, + SignerListSet, + TrustSet, + Wallet, + XChainAddClaimAttestation, + XChainBridge, + XChainCreateBridge, + XChainCreateClaimID, + xrpToDrops, +} from '../../../src' +import serverUrl from '../serverUrl' +import { + setupBridge, + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { + generateFundedWallet, + getIOUBalance, + getXRPBalance, + testTransaction, +} from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateBridge', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge, witness, signatureReward } = await setupBridge( + testContext.client, + ) + const otherChainSource = Wallet.generate() + const amount = xrpToDrops(10) + + const claimIdTx: XChainCreateClaimID = { + TransactionType: 'XChainCreateClaimID', + Account: testContext.wallet.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + OtherChainSource: otherChainSource.classicAddress, + } + await testTransaction(testContext.client, claimIdTx, testContext.wallet) + + const initialBalance = Number( + await getXRPBalance(testContext.client, testContext.wallet), + ) + + const attestationToSign = { + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + AttestationRewardAccount: witness.classicAddress, + WasLockingChainSend: 0, + XChainClaimID: 1, + Destination: testContext.wallet.classicAddress, + } + const encodedAttestation = encode(attestationToSign) + const attestationSignature = sign(encodedAttestation, witness.privateKey) + + const tx: XChainAddClaimAttestation = { + TransactionType: 'XChainAddClaimAttestation', + Account: witness.classicAddress, + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + WasLockingChainSend: 0, + XChainClaimID: 1, + Destination: testContext.wallet.classicAddress, + PublicKey: witness.publicKey, + Signature: attestationSignature, + AttestationRewardAccount: witness.classicAddress, + AttestationSignerAccount: witness.classicAddress, + } + await testTransaction(testContext.client, tx, witness) + + const finalBalance = Number( + await getXRPBalance(testContext.client, testContext.wallet), + ) + assert.equal( + finalBalance, + initialBalance + Number(amount) - Number(signatureReward), + 'The destination balance should go up by the amount transferred', + ) + }, + TIMEOUT, + ) + + it( + 'IOU', + async () => { + const witness = await generateFundedWallet(testContext.client) + // we are on the "issuing chain" for this test + const lockingDoor = Wallet.generate() + const issuer = Wallet.generate() + + // set default rippling + const defaultRipplingTx: AccountSet = { + TransactionType: 'AccountSet', + Account: testContext.wallet.classicAddress, + SetFlag: AccountSetAsfFlags.asfDefaultRipple, + } + await testTransaction( + testContext.client, + defaultRipplingTx, + testContext.wallet, + ) + + const destination = await generateFundedWallet(testContext.client) + + const trustlineTx: TrustSet = { + TransactionType: 'TrustSet', + Account: destination.classicAddress, + LimitAmount: { + currency: 'USD', + issuer: testContext.wallet.classicAddress, + value: '1000000000', + }, + } + await testTransaction(testContext.client, trustlineTx, destination) + + const signatureReward = '200' + const xchainBridge: XChainBridge = { + LockingChainDoor: lockingDoor.classicAddress, + LockingChainIssue: { + currency: 'USD', + issuer: issuer.classicAddress, + }, + IssuingChainDoor: testContext.wallet.classicAddress, + IssuingChainIssue: { + currency: 'USD', + issuer: testContext.wallet.classicAddress, + }, + } + const setupTx: XChainCreateBridge = { + TransactionType: 'XChainCreateBridge', + Account: testContext.wallet.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + } + + await testTransaction(testContext.client, setupTx, testContext.wallet) + + // confirm that the transaction actually went through + const accountObjectsResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'bridge', + }) + assert.lengthOf( + accountObjectsResponse.result.account_objects, + 1, + 'Should be exactly one bridge owned by the account', + ) + + const signerTx: SignerListSet = { + TransactionType: 'SignerListSet', + Account: testContext.wallet.classicAddress, + SignerEntries: [ + { + SignerEntry: { + Account: witness.classicAddress, + SignerWeight: 1, + }, + }, + ], + SignerQuorum: 1, + } + await testTransaction(testContext.client, signerTx, testContext.wallet) + + const signerAccountInfoResponse = await testContext.client.request({ + command: 'account_info', + account: testContext.wallet.classicAddress, + signer_lists: true, + }) + const signerListInfo = + signerAccountInfoResponse.result.account_data.signer_lists?.[0] + assert.deepEqual( + signerListInfo?.SignerEntries, + signerTx.SignerEntries, + 'SignerEntries were not set properly', + ) + assert.equal( + signerListInfo?.SignerQuorum, + signerTx.SignerQuorum, + 'SignerQuorum was not set properly', + ) + + const otherChainSource = Wallet.generate() + const amount: IssuedCurrencyAmount = { + currency: 'USD', + issuer: issuer.classicAddress, + value: '10', + } + + const claimIdTx: XChainCreateClaimID = { + TransactionType: 'XChainCreateClaimID', + Account: destination.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + OtherChainSource: otherChainSource.classicAddress, + } + await testTransaction(testContext.client, claimIdTx, destination) + + const initialBalance = Number( + await getIOUBalance( + testContext.client, + destination, + xchainBridge.IssuingChainIssue as IssuedCurrency, + ), + ) + + const attestationToSign = { + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + AttestationRewardAccount: witness.classicAddress, + WasLockingChainSend: 1, + XChainClaimID: 1, + Destination: destination.classicAddress, + } + const encodedAttestation = encode(attestationToSign) + const attestationSignature = sign(encodedAttestation, witness.privateKey) + + const tx: XChainAddClaimAttestation = { + TransactionType: 'XChainAddClaimAttestation', + Account: witness.classicAddress, + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + WasLockingChainSend: 1, + XChainClaimID: 1, + Destination: destination.classicAddress, + PublicKey: witness.publicKey, + Signature: attestationSignature, + AttestationRewardAccount: witness.classicAddress, + AttestationSignerAccount: witness.classicAddress, + } + await testTransaction(testContext.client, tx, witness) + + const finalBalance = Number( + await getIOUBalance( + testContext.client, + destination, + xchainBridge.IssuingChainIssue as IssuedCurrency, + ), + ) + assert.equal( + finalBalance, + initialBalance + Number(amount.value), + 'The destination balance should go up by the amount transferred', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainClaim.test.ts b/packages/xrpl/test/integration/transactions/xchainClaim.test.ts new file mode 100644 index 00000000..1987ceec --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainClaim.test.ts @@ -0,0 +1,112 @@ +import { assert } from 'chai' +import { encode } from 'ripple-binary-codec' +import { sign } from 'ripple-keypairs' + +import { + Wallet, + XChainAddClaimAttestation, + XChainClaim, + XChainCreateClaimID, + xrpToDrops, +} from '../../../src' +import serverUrl from '../serverUrl' +import { + setupBridge, + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { generateFundedWallet, getXRPBalance, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateBridge', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge, witness, signatureReward } = await setupBridge( + testContext.client, + ) + + const destination = await generateFundedWallet(testContext.client) + const otherChainSource = Wallet.generate() + const amount = xrpToDrops(10) + + const claimIdTx: XChainCreateClaimID = { + TransactionType: 'XChainCreateClaimID', + Account: destination.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + OtherChainSource: otherChainSource.classicAddress, + } + await testTransaction(testContext.client, claimIdTx, destination) + + const initialBalance = Number( + await getXRPBalance(testContext.client, destination), + ) + + const attestationToSign = { + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + AttestationRewardAccount: witness.classicAddress, + WasLockingChainSend: 0, + XChainClaimID: 1, + } + const encodedAttestation = encode(attestationToSign) + const attestationSignature = sign(encodedAttestation, witness.privateKey) + + const claimTx: XChainAddClaimAttestation = { + TransactionType: 'XChainAddClaimAttestation', + Account: witness.classicAddress, + XChainBridge: xchainBridge, + OtherChainSource: otherChainSource.classicAddress, + Amount: amount, + WasLockingChainSend: 0, + XChainClaimID: 1, + PublicKey: witness.publicKey, + Signature: attestationSignature, + AttestationRewardAccount: witness.classicAddress, + AttestationSignerAccount: witness.classicAddress, + } + await testTransaction(testContext.client, claimTx, witness) + + const intermediateBalance = Number( + await getXRPBalance(testContext.client, destination), + ) + assert.equal( + initialBalance, + intermediateBalance, + "The destination's balance should not change yet", + ) + + const tx: XChainClaim = { + TransactionType: 'XChainClaim', + Account: destination.classicAddress, + XChainBridge: xchainBridge, + Destination: destination.classicAddress, + XChainClaimID: 1, + Amount: amount, + } + await testTransaction(testContext.client, tx, destination) + + const finalBalance = Number( + await getXRPBalance(testContext.client, destination), + ) + assert.equal( + finalBalance, + initialBalance + Number(amount) - Number(signatureReward) - 12, + "The destination's balance should not change yet", + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainCommit.test.ts b/packages/xrpl/test/integration/transactions/xchainCommit.test.ts new file mode 100644 index 00000000..f041ac4c --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainCommit.test.ts @@ -0,0 +1,61 @@ +import { assert } from 'chai' + +import { XChainCommit } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupBridge, + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { generateFundedWallet, getXRPBalance, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCommit', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge } = await setupBridge(testContext.client) + + const initialBalance = Number( + await getXRPBalance(testContext.client, xchainBridge.LockingChainDoor), + ) + + // actually test XChainCommit + const wallet2 = await generateFundedWallet(testContext.client) + const amount = 10000000 + const tx: XChainCommit = { + TransactionType: 'XChainCommit', + Account: wallet2.classicAddress, + XChainBridge: xchainBridge, + XChainClaimID: 1, + Amount: amount.toString(), + } + + await testTransaction(testContext.client, tx, wallet2) + + const accountInfoResponse2 = await testContext.client.request({ + command: 'account_info', + account: xchainBridge.LockingChainDoor, + }) + const finalBalance = Number( + accountInfoResponse2.result.account_data.Balance, + ) + assert.equal( + initialBalance + amount, + finalBalance, + "The bridge door's balance should go up by the amount committed", + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainCreateBridge.test.ts b/packages/xrpl/test/integration/transactions/xchainCreateBridge.test.ts new file mode 100644 index 00000000..65d5f961 --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainCreateBridge.test.ts @@ -0,0 +1,55 @@ +import { assert } from 'chai' + +import { XChainCreateBridge } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { GENESIS_ACCOUNT, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateBridge', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const tx: XChainCreateBridge = { + TransactionType: 'XChainCreateBridge', + Account: testContext.wallet.classicAddress, + XChainBridge: { + LockingChainDoor: testContext.wallet.classicAddress, + LockingChainIssue: { currency: 'XRP' }, + IssuingChainDoor: GENESIS_ACCOUNT, + IssuingChainIssue: { currency: 'XRP' }, + }, + SignatureReward: '200', + MinAccountCreateAmount: '10000000', + } + + await testTransaction(testContext.client, tx, testContext.wallet) + + // confirm that the transaction actually went through + const accountObjectsResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'bridge', + }) + assert.lengthOf( + accountObjectsResponse.result.account_objects, + 1, + 'Should be exactly one bridge owned by the account', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainCreateClaimID.test.ts b/packages/xrpl/test/integration/transactions/xchainCreateClaimID.test.ts new file mode 100644 index 00000000..7552f7b9 --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainCreateClaimID.test.ts @@ -0,0 +1,57 @@ +import { assert } from 'chai' + +import { XChainCreateClaimID, Wallet } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupBridge, + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { generateFundedWallet, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateClaimID', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const { xchainBridge, signatureReward } = await setupBridge( + testContext.client, + ) + + // actually test XChainCreateClaimID + const wallet2 = await generateFundedWallet(testContext.client) + const otherChainSource = Wallet.generate() + const tx: XChainCreateClaimID = { + TransactionType: 'XChainCreateClaimID', + Account: wallet2.classicAddress, + XChainBridge: xchainBridge, + SignatureReward: signatureReward, + OtherChainSource: otherChainSource.classicAddress, + } + + await testTransaction(testContext.client, tx, wallet2) + + const accountObjectsResponse2 = await testContext.client.request({ + command: 'account_objects', + account: wallet2.classicAddress, + type: 'xchain_owned_claim_id', + }) + assert.lengthOf( + accountObjectsResponse2.result.account_objects, + 1, + 'Should be exactly one claim ID owned by the account', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/xchainModifyBridge.test.ts b/packages/xrpl/test/integration/transactions/xchainModifyBridge.test.ts new file mode 100644 index 00000000..5910ea9a --- /dev/null +++ b/packages/xrpl/test/integration/transactions/xchainModifyBridge.test.ts @@ -0,0 +1,95 @@ +import { assert } from 'chai' + +import { XChainCreateBridge, XChainModifyBridge } from '../../../src' +import { Bridge } from '../../../src/models/ledger' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { GENESIS_ACCOUNT, testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('XChainCreateBridge', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const setupTx: XChainCreateBridge = { + TransactionType: 'XChainCreateBridge', + Account: testContext.wallet.classicAddress, + XChainBridge: { + LockingChainDoor: testContext.wallet.classicAddress, + LockingChainIssue: { currency: 'XRP' }, + IssuingChainDoor: GENESIS_ACCOUNT, + IssuingChainIssue: { currency: 'XRP' }, + }, + SignatureReward: '200', + MinAccountCreateAmount: '10000000', + } + + await testTransaction(testContext.client, setupTx, testContext.wallet) + + // confirm that the transaction actually went through + const accountObjectsResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'bridge', + }) + assert.lengthOf( + accountObjectsResponse.result.account_objects, + 1, + 'Should be exactly one bridge owned by the account', + ) + const initialBridge = accountObjectsResponse.result + .account_objects[0] as unknown as Bridge + assert.equal( + initialBridge.SignatureReward, + '200', + 'Signature reward is incorrect', + ) + + const tx: XChainModifyBridge = { + TransactionType: 'XChainModifyBridge', + Account: testContext.wallet.classicAddress, + XChainBridge: { + LockingChainDoor: testContext.wallet.classicAddress, + LockingChainIssue: { currency: 'XRP' }, + IssuingChainDoor: GENESIS_ACCOUNT, + IssuingChainIssue: { currency: 'XRP' }, + }, + SignatureReward: '300', + } + await testTransaction(testContext.client, tx, testContext.wallet) + + // confirm that the transaction actually went through + const accountObjectsResponse2 = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'bridge', + }) + assert.lengthOf( + accountObjectsResponse2.result.account_objects, + 1, + 'Should be exactly one bridge owned by the account', + ) + const finalBridge = accountObjectsResponse2.result + .account_objects[0] as unknown as Bridge + assert.equal( + finalBridge.SignatureReward, + '300', + 'Signature reward was not modified', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/utils.ts b/packages/xrpl/test/integration/utils.ts index 6387d3c6..9183eb13 100644 --- a/packages/xrpl/test/integration/utils.ts +++ b/packages/xrpl/test/integration/utils.ts @@ -10,12 +10,14 @@ import { type SubmitResponse, TimeoutError, NotConnectedError, + AccountLinesRequest, + IssuedCurrency, } from '../../src' import { Payment, Transaction } from '../../src/models/transactions' import { hashSignedTx } from '../../src/utils/hashes' -const masterAccount = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' -const masterSecret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' +export const GENESIS_ACCOUNT = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' +const GENESIS_SECRET = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' export async function sendLedgerAccept(client: Client): Promise { return client.connection.request({ command: 'ledger_accept' }) @@ -141,12 +143,12 @@ export async function fundAccount( ): Promise { const payment: Payment = { TransactionType: 'Payment', - Account: masterAccount, + Account: GENESIS_ACCOUNT, Destination: wallet.classicAddress, // 2 times the amount needed for a new account (20 XRP) Amount: '400000000', } - const wal = Wallet.fromSeed(masterSecret) + const wal = Wallet.fromSeed(GENESIS_SECRET) const response = await submitTransaction({ client, wallet: wal, @@ -263,11 +265,13 @@ export async function testTransaction( export async function getXRPBalance( client: Client, - wallet: Wallet, + account: string | Wallet, ): Promise { + const address: string = + typeof account === 'string' ? account : account.classicAddress const request: AccountInfoRequest = { command: 'account_info', - account: wallet.classicAddress, + account: address, } return (await client.request(request)).result.account_data.Balance } @@ -338,3 +342,16 @@ export async function waitForAndForceProgressLedgerTime( throw new Error(`Ledger time not reached after ${retries} retries.`) } + +export async function getIOUBalance( + client: Client, + wallet: Wallet, + currency: IssuedCurrency, +): Promise { + const request: AccountLinesRequest = { + command: 'account_lines', + account: wallet.classicAddress, + peer: currency.issuer, + } + return (await client.request(request)).result.lines[0].balance +}