diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 92fa2bed..df2979bf 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -21,7 +21,7 @@ "UInt32": 2, "STArray": 15, "Sidechain": 24, - "XchainClaimProof": 25 + "XChainClaimProof": 25 }, "LEDGER_ENTRY_TYPES": { "Any": -3, diff --git a/packages/ripple-binary-codec/src/types/amount.ts b/packages/ripple-binary-codec/src/types/amount.ts index 9d961058..d4695502 100644 --- a/packages/ripple-binary-codec/src/types/amount.ts +++ b/packages/ripple-binary-codec/src/types/amount.ts @@ -147,7 +147,8 @@ class Amount extends SerializedType { */ toJSON(): AmountObject | string { if (this.isNative()) { - const bytes = this.bytes + const bytes = Buffer.alloc(this.bytes.length) + this.bytes.copy(bytes) const isPositive = bytes[0] & 0x40 const sign = isPositive ? '' : '-' bytes[0] &= 0x3f diff --git a/packages/ripple-binary-codec/src/types/index.ts b/packages/ripple-binary-codec/src/types/index.ts index 014aa12e..9ebccfe0 100644 --- a/packages/ripple-binary-codec/src/types/index.ts +++ b/packages/ripple-binary-codec/src/types/index.ts @@ -14,6 +14,7 @@ import { Hash256 } from './hash-256' import { IssuedCurrency } from './issued-currency' import { PathSet } from './path-set' import { Sidechain } from './sidechain' +import { Signature } from './signature' import { STArray } from './st-array' import { STObject } from './st-object' import { UInt16 } from './uint-16' @@ -21,6 +22,7 @@ import { UInt32 } from './uint-32' import { UInt64 } from './uint-64' import { UInt8 } from './uint-8' import { Vector256 } from './vector-256' +import { XChainClaimProof } from './xchain-claim-proof' const coreTypes = { AccountID, @@ -33,6 +35,7 @@ const coreTypes = { IssuedCurrency, PathSet, Sidechain, + Signature, STArray, STObject, UInt8, @@ -40,6 +43,7 @@ const coreTypes = { UInt32, UInt64, Vector256, + XChainClaimProof, } Object.values(Field).forEach((field) => { diff --git a/packages/ripple-binary-codec/src/types/signature.ts b/packages/ripple-binary-codec/src/types/signature.ts new file mode 100644 index 00000000..5967d015 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/signature.ts @@ -0,0 +1,131 @@ +import { BinaryParser } from '../serdes/binary-parser' + +import { JsonObject, SerializedType } from './serialized-type' +import { Buffer } from 'buffer/' +import { Blob } from './blob' +import { decodeAccountPublic, encodeAccountPublic } from 'ripple-address-codec' + +/** + * Interface for JSON objects that represent amounts + */ +interface SignatureObject extends JsonObject { + signature: string + signing_key: string +} + +/** + * Type guard for AmountObject + */ +function isSignatureObject(arg): arg is SignatureObject { + const keys = Object.keys(arg).sort() + return ( + keys.length === 2 && keys[0] === 'signature' && keys[1] === 'signing_key' + ) +} + +function encodeVariableLength(length: number): Buffer { + const lenBytes = Buffer.alloc(3) + if (length <= 192) { + lenBytes[0] = length + return lenBytes.slice(0, 1) + } else if (length <= 12480) { + length -= 193 + lenBytes[0] = 193 + (length >>> 8) + lenBytes[1] = length & 0xff + return lenBytes.slice(0, 2) + } else if (length <= 918744) { + length -= 12481 + lenBytes[0] = 241 + (length >>> 16) + lenBytes[1] = (length >> 8) & 0xff + lenBytes[2] = length & 0xff + return lenBytes.slice(0, 3) + } + throw new Error('Overflow error') +} + +/** + * Class for serializing/Deserializing Amounts + */ +class Signature extends SerializedType { + static readonly ZERO_SIGNATURE: Signature = new Signature( + Buffer.concat([Buffer.alloc(1), Buffer.from([33]), Buffer.alloc(33)]), + ) + + constructor(bytes: Buffer) { + super(bytes ?? Signature.ZERO_SIGNATURE.bytes) + } + + /** + * Construct an amount from an IOU or string amount + * + * @param value An Amount, object representing an IOU, or a string + * representing an integer amount + * @returns An Amount object + */ + static from(value: T): Signature { + if (value instanceof Signature) { + return value + } + + if (isSignatureObject(value)) { + const signature = Blob.from(value.signature).toBytes() + const signing_key = new Blob( + Buffer.from(decodeAccountPublic(value.signing_key)), + ).toBytes() + return new Signature( + Buffer.concat([ + encodeVariableLength(signature.length), + signature, + encodeVariableLength(signing_key.length), + signing_key, + ]), + ) + } + + throw new Error('Invalid type to construct a Signature') + } + + /** + * Read an amount from a BinaryParser + * + * @param parser BinaryParser to read the Amount from + * @returns An Amount object + */ + static fromParser(parser: BinaryParser): Signature { + const bytes: Array = [] + + const signatureLength = parser.readVariableLengthLength() + bytes.push(encodeVariableLength(signatureLength)) + bytes.push(Blob.fromParser(parser, signatureLength).toBytes()) + const signingKeyLength = parser.readVariableLengthLength() + bytes.push(encodeVariableLength(signingKeyLength)) + bytes.push(Blob.fromParser(parser, signingKeyLength).toBytes()) + + return new Signature(Buffer.concat(bytes)) + } + + /** + * Get the JSON representation of this Amount + * + * @returns the JSON interpretation of this.bytes + */ + toJSON(): SignatureObject { + const parser = new BinaryParser(this.toString()) + const signatureLength = parser.readVariableLengthLength() + const signature = Blob.fromParser( + parser, + signatureLength, + ).toJSON() as string + const signingKeyLength = parser.readVariableLengthLength() + const signingKey = Blob.fromParser(parser, signingKeyLength) + + return { + signature, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + signing_key: encodeAccountPublic(signingKey.toBytes()), + } + } +} + +export { Signature, SignatureObject } diff --git a/packages/ripple-binary-codec/src/types/uint-32.ts b/packages/ripple-binary-codec/src/types/uint-32.ts index aea301d9..432b9eb3 100644 --- a/packages/ripple-binary-codec/src/types/uint-32.ts +++ b/packages/ripple-binary-codec/src/types/uint-32.ts @@ -6,7 +6,7 @@ import { Buffer } from 'buffer/' * Derived UInt class for serializing/deserializing 32 bit UInt */ class UInt32 extends UInt { - protected static readonly width: number = 32 / 8 // 4 + public static readonly width: number = 32 / 8 // 4 static readonly defaultUInt32: UInt32 = new UInt32(Buffer.alloc(UInt32.width)) constructor(bytes: Buffer) { diff --git a/packages/ripple-binary-codec/src/types/uint-8.ts b/packages/ripple-binary-codec/src/types/uint-8.ts index d83e1405..33a9bd54 100644 --- a/packages/ripple-binary-codec/src/types/uint-8.ts +++ b/packages/ripple-binary-codec/src/types/uint-8.ts @@ -6,7 +6,7 @@ import { Buffer } from 'buffer/' * Derived UInt class for serializing/deserializing 8 bit UInt */ class UInt8 extends UInt { - protected static readonly width: number = 8 / 8 // 1 + public static readonly width: number = 8 / 8 // 1 static readonly defaultUInt8: UInt8 = new UInt8(Buffer.alloc(UInt8.width)) constructor(bytes: Buffer) { diff --git a/packages/ripple-binary-codec/src/types/xchain-claim-proof.ts b/packages/ripple-binary-codec/src/types/xchain-claim-proof.ts new file mode 100644 index 00000000..cef2005e --- /dev/null +++ b/packages/ripple-binary-codec/src/types/xchain-claim-proof.ts @@ -0,0 +1,157 @@ +import { BinaryParser } from '../serdes/binary-parser' + +import { JsonObject, SerializedType } from './serialized-type' +import { Buffer } from 'buffer/' +import { Sidechain, SidechainObject } from './sidechain' +import { Signature, SignatureObject } from './signature' +import { Amount } from './amount' +import { UInt8 } from './uint-8' +import { UInt32 } from './uint-32' + +/** + * Constants for separating Paths in a PathSet + */ +const ARRAY_END_BYTE = 0xf1 + +/** + * Interface for JSON objects that represent amounts + */ +interface XChainClaimProofObject extends JsonObject { + amount: string + sidechain: SidechainObject + signatures: SignatureObject[] + was_src_chain_send: boolean + xchain_seq: number +} + +/** + * Type guard for AmountObject + */ +function isProofObject(arg): arg is XChainClaimProofObject { + const keys = Object.keys(arg).sort() + return ( + keys.length === 5 && + keys[0] === 'amount' && + keys[1] === 'sidechain' && + keys[2] === 'signatures' && + keys[3] === 'was_src_chain_send' && + keys[4] === 'xchain_seq' + ) +} + +/** + * Class for serializing/Deserializing Amounts + */ +class XChainClaimProof extends SerializedType { + static readonly ZERO_PROOF: XChainClaimProof = new XChainClaimProof( + Buffer.concat([ + Amount.defaultAmount.toBytes(), + Sidechain.ZERO_SIDECHAIN.toBytes(), + Buffer.from([ARRAY_END_BYTE]), + UInt8.defaultUInt8.toBytes(), + UInt32.defaultUInt32.toBytes(), + ]), + ) + + constructor(bytes: Buffer) { + super(bytes ?? XChainClaimProof.ZERO_PROOF.bytes) + } + + /** + * Construct an amount from an IOU or string amount + * + * @param value An Amount, object representing an IOU, or a string + * representing an integer amount + * @returns An Amount object + */ + static from( + value: T, + ): XChainClaimProof { + if (value instanceof XChainClaimProof) { + return value + } + + if (isProofObject(value)) { + const amount = Amount.from(value.amount).toBytes() + const sidechain = Sidechain.from(value.sidechain).toBytes() + const signatures: Array = [] + value.signatures.forEach((signature: SignatureObject) => { + signatures.push(Signature.from(signature).toBytes()) + }) + signatures.push(Buffer.from([ARRAY_END_BYTE])) + const was_src_chain_send = UInt8.from( + Number(value.was_src_chain_send), + ).toBytes() + const xchain_seq = UInt32.from(value.xchain_seq).toBytes() + return new XChainClaimProof( + Buffer.concat([ + amount, + sidechain, + ...signatures, + was_src_chain_send, + xchain_seq, + ]), + ) + } + + throw new Error('Invalid type to construct a XChainClaimProof') + } + + /** + * Read an amount from a BinaryParser + * + * @param parser BinaryParser to read the Amount from + * @returns An Amount object + */ + static fromParser(parser: BinaryParser): XChainClaimProof { + const bytes: Array = [] + + bytes.push(Amount.fromParser(parser).toBytes()) + bytes.push(Sidechain.fromParser(parser).toBytes()) + while (!parser.end()) { + bytes.push(Signature.fromParser(parser).toBytes()) + + if (parser.peek() === ARRAY_END_BYTE) { + bytes.push(parser.read(1)) + break + } + } + bytes.push(parser.read(UInt8.width)) + bytes.push(parser.read(UInt32.width)) + + return new XChainClaimProof(Buffer.concat(bytes)) + } + + /** + * Get the JSON representation of this Amount + * + * @returns the JSON interpretation of this.bytes + */ + toJSON(): XChainClaimProofObject { + const parser = new BinaryParser(this.toString()) + const amount = Amount.fromParser(parser).toJSON() + const sidechain = Sidechain.fromParser(parser).toJSON() + const signatures: SignatureObject[] = [] + while (!parser.end()) { + if (parser.peek() === ARRAY_END_BYTE) { + parser.skip(1) + break + } + + signatures.push(Signature.fromParser(parser).toJSON()) + } + const was_src_chain_send = UInt8.fromParser(parser).toJSON() + + const xchain_seq = UInt32.fromParser(parser).toJSON() + + return { + amount: amount as string, + sidechain, + signatures, + was_src_chain_send: Boolean(was_src_chain_send), + xchain_seq: xchain_seq as number, + } + } +} + +export { XChainClaimProof, XChainClaimProofObject } diff --git a/packages/ripple-binary-codec/test/binary-serializer.test.js b/packages/ripple-binary-codec/test/binary-serializer.test.js index 25478df0..fc8662a1 100644 --- a/packages/ripple-binary-codec/test/binary-serializer.test.js +++ b/packages/ripple-binary-codec/test/binary-serializer.test.js @@ -108,6 +108,7 @@ const NegativeUNL = require('./fixtures/negative-unl.json') const XChainDoorCreate = require('./fixtures/xchain-door-create.json') const XChainSeqNumCreate = require('./fixtures/xchain-seqnum-create.json') const XChainTransfer = require('./fixtures/xchain-transfer.json') +const XChainClaim = require('./fixtures/xchain-claim.json') function bytesListTest() { const list = new BytesList() @@ -251,6 +252,12 @@ function sidechainTest() { test('can deserialize XChainTransfer', () => { expect(decode(XChainTransfer.binary)).toEqual(XChainTransfer.tx) }) + test('can serialize XChainClaim', () => { + expect(encode(XChainClaim.tx)).toEqual(XChainClaim.binary) + }) + test('can deserialize XChainClaim', () => { + expect(decode(XChainClaim.binary)).toEqual(XChainClaim.tx) + }) } function omitUndefinedTest() { diff --git a/packages/ripple-binary-codec/test/fixtures/xchain-claim.json b/packages/ripple-binary-codec/test/fixtures/xchain-claim.json new file mode 100644 index 00000000..8ac60d4d --- /dev/null +++ b/packages/ripple-binary-codec/test/fixtures/xchain-claim.json @@ -0,0 +1,50 @@ +{ + "binary": "12002122800000008114621D345F8F094A085132431C69C89EC05D212CC28314CA64525733C3BEED910CFE2AE280D3C078DABB4B0119400000003B9ACA00CC86E58C9B58D4CF71CB8C1B41F21BB290CE13D40000000000000000000000000000000000000000C48CAD01682D7A86296EF14523074D4852C02EA900000000000000000000000000000000000000004730450221008CC9842A6855A37131FE7FB978675DCF329AC5CD7C881FAF6D13CDC23363059F02203A10475640C2C09541A55098109BB3326D3F49E2304710736A4E3C2773539B012103ADB44CA8E56F78A0096825E5667C450ABD5C24C34E027BC1AAF7E5BD114CB5B5473045022100B93B51FDBE42634828EEF7F2E5FA950557283648C5D39196ED1B08F8B394959102201F162F43B41D0D9316BF3B7AFA3BC9CA0EC1FCECF6EE8C94F58616BEA2D31F2C2102A14E886B3C3579FBAE3139F29728B903E6F4295AEE92160C8480695524D66A154630440220254038C5D6106246AEBB2A94E60CE79102321FDFD0468AA40C5E7F2DF1C205FE02204CEE5B3BFFDDCB67AE593348E8D8551E82770415BC9581EF0EF20DD000DD550C2102F7390DCF3352060847B81666EBAC79D52DEA2443BDF58439F75397C45334E2DC473045022100AA28592882A3B7C769B32564EDF9F816179D42D8C0E3988567F816E9A5453C6002202BA57DC085B9EB8427FBD58E05B3617183AC216EC7066F03FA4896D8B449490E2102498BD8CD9CA6A4BA567A2ECFA163F118AFD30511CBBA71429C2EC2F74D760592473045022100B9D649491723810B705282B66EAD6075235A1954BEEE054FF68066FADE11DA1C02205D977AE7C6706224A5A3AA75A53E13CA3DE9D67084405CD9BB474554EEE5C0702103219642288DEE8A3AA8FEA1F7DAE9ED4D9A9F0EADA1E2DE3DB56DD9598D9AD817F10100000001", + "tx": { + "Account" : "r9A8UyNpW3X46FUc6P7JZqgn6WgAPjBwPg", + "Destination" : "rKT9gDkaedAosiHyHZTjyZs2HvXpzuiGmC", + "Flags" : 2147483648, + "TransactionType" : "XChainClaim", + "XChainClaimProof" : + { + "amount" : "1000000000", + "sidechain" : + { + "dst_chain_door" : "rKeSSvHvaMZJp9ykaxutVwkhZgWuWMLnQt", + "dst_chain_issue" : "XRP", + "src_chain_door" : "rJvExveLEL4jNDEeLKCVdxaSCN9cEBnEQC", + "src_chain_issue" : "XRP" + }, + "signatures" : + [ + + { + "signature" : "30450221008CC9842A6855A37131FE7FB978675DCF329AC5CD7C881FAF6D13CDC23363059F02203A10475640C2C09541A55098109BB3326D3F49E2304710736A4E3C2773539B01", + "signing_key" : "aBRDkDojKThV2dNbjvxQDwxtCLkgpd16bVFNCRhGMQKrG1VjGQmi" + }, + + { + "signature" : "3045022100B93B51FDBE42634828EEF7F2E5FA950557283648C5D39196ED1B08F8B394959102201F162F43B41D0D9316BF3B7AFA3BC9CA0EC1FCECF6EE8C94F58616BEA2D31F2C", + "signing_key" : "aBPBY4NyQAGwBHLEYchMNtsGPLmsCDeqXs3dvvTuZffYxgH6pb5Y" + }, + + { + "signature" : "30440220254038C5D6106246AEBB2A94E60CE79102321FDFD0468AA40C5E7F2DF1C205FE02204CEE5B3BFFDDCB67AE593348E8D8551E82770415BC9581EF0EF20DD000DD550C", + "signing_key" : "aBPq4y6k28jQNzcNMESzFPhwt1gdNTQCzBMD6iSAW8rEqvuX2fi6" + }, + + { + "signature" : "3045022100AA28592882A3B7C769B32564EDF9F816179D42D8C0E3988567F816E9A5453C6002202BA57DC085B9EB8427FBD58E05B3617183AC216EC7066F03FA4896D8B449490E", + "signing_key" : "aB4WteEVakaAcnKq6VDxVA812ehqgr25XpQnycM96RjHUrrh76MH" + }, + + { + "signature" : "3045022100B9D649491723810B705282B66EAD6075235A1954BEEE054FF68066FADE11DA1C02205D977AE7C6706224A5A3AA75A53E13CA3DE9D67084405CD9BB474554EEE5C070", + "signing_key" : "aBQwsfTh1zikjFDLpkihw6Ug3iyeeHP93P3rZ8pwTDJe7veiRMSH" + } + ], + "was_src_chain_send" : true, + "xchain_seq" : 1 + } + } +} diff --git a/packages/ripple-binary-codec/test/types.test.js b/packages/ripple-binary-codec/test/types.test.js index afc79ab8..797316f6 100644 --- a/packages/ripple-binary-codec/test/types.test.js +++ b/packages/ripple-binary-codec/test/types.test.js @@ -21,6 +21,7 @@ describe('SerializedType interfaces', () => { }) test(`${name}.from(json).toJSON() == json`, () => { const newJSON = new Value().toJSON() + console.log(newJSON) expect(Value.from(newJSON).toJSON()).toEqual(newJSON) }) describe(`${name} supports all methods of the SerializedType mixin`, () => {