diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 19ab2820be..1ab810e449 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -21,6 +21,7 @@ "UInt192": 21, "UInt384": 22, "UInt512": 23, + "Issue": 24, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -44,6 +45,7 @@ "NegativeUNL": 78, "NFTokenPage": 80, "NFTokenOffer": 55, + "AMM": 121, "Any": -3, "Child": -2, "Nickname": 110, @@ -271,6 +273,26 @@ "type": "UInt16" } ], + [ + "TradingFee", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], + [ + "DiscountedFee", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], [ "Version", { @@ -771,6 +793,26 @@ "type": "UInt32" } ], + [ + "VoteWeight", + { + "nth": 48, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "FirstNFTokenSequence", + { + "nth": 50, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1121,6 +1163,16 @@ "type": "Hash256" } ], + [ + "AMMID", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "BookDirectory", { @@ -1391,6 +1443,36 @@ "type": "Amount" } ], + [ + "Amount2", + { + "nth": 11, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "BidMin", + { + "nth": 12, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "BidMax", + { + "nth": 13, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "MinimumOffer", { @@ -1431,6 +1513,86 @@ "type": "Amount" } ], + [ + "BaseFeeDrops", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "ReserveBaseDrops", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "ReserveIncrementDrops", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenOut", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenIn", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "EPrice", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "Price", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenBalance", + { + "nth": 31, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "PublicKey", { @@ -1821,6 +1983,26 @@ "type": "PathSet" } ], + [ + "Asset", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], + [ + "Asset2", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], [ "TransactionMetaData", { @@ -2031,6 +2213,36 @@ "type": "STObject" } ], + [ + "VoteEntry", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuctionSlot", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuthAccount", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2121,6 +2333,16 @@ "type": "STArray" } ], + [ + "VoteSlots", + { + "nth": 12, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "Majorities", { @@ -2170,6 +2392,16 @@ "isSigningField": true, "type": "STArray" } + ], + [ + "AuthAccounts", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } ] ], "TRANSACTION_RESULTS": { @@ -2228,6 +2460,7 @@ "temUNKNOWN": -264, "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, + "temBAD_AMM_TOKENS": -261, "tefFAILURE": -199, "tefALREADY": -198, @@ -2263,6 +2496,7 @@ "terNO_RIPPLE": -90, "terQUEUED": -89, "terPRE_TICKET": -88, + "terNO_AMM": -87, "tesSUCCESS": 0, @@ -2311,7 +2545,15 @@ "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, "tecINSUFFICIENT_FUNDS": 159, "tecOBJECT_NOT_FOUND": 160, - "tecINSUFFICIENT_PAYMENT": 161 + "tecINSUFFICIENT_PAYMENT": 161, + "tecUNFUNDED_AMM": 162, + "tecAMM_BALANCE": 163, + "tecAMM_FAILED": 164, + "tecAMM_INVALID_TOKENS": 165, + "tecAMM_EMPTY": 166, + "tecAMM_NOT_EMPTY": 167, + "tecAMM_ACCOUNT": 168, + "tecINCOMPLETE": 169 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2344,6 +2586,12 @@ "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, "Clawback": 30, + "AMMCreate": 35, + "AMMDeposit": 36, + "AMMWithdraw": 37, + "AMMVote": 38, + "AMMBid": 39, + "AMMDelete": 40, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/packages/ripple-binary-codec/src/types/index.ts b/packages/ripple-binary-codec/src/types/index.ts index ffd7278673..7b98d93510 100644 --- a/packages/ripple-binary-codec/src/types/index.ts +++ b/packages/ripple-binary-codec/src/types/index.ts @@ -5,6 +5,7 @@ import { Currency } from './currency' import { Hash128 } from './hash-128' import { Hash160 } from './hash-160' import { Hash256 } from './hash-256' +import { Issue } from './issue' import { PathSet } from './path-set' import { STArray } from './st-array' import { STObject } from './st-object' @@ -24,6 +25,7 @@ const coreTypes: Record = { Hash128, Hash160, Hash256, + Issue, PathSet, STArray, STObject, diff --git a/packages/ripple-binary-codec/src/types/issue.ts b/packages/ripple-binary-codec/src/types/issue.ts new file mode 100644 index 0000000000..3c3925b945 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/issue.ts @@ -0,0 +1,96 @@ +import { BinaryParser } from '../serdes/binary-parser' + +import { AccountID } from './account-id' +import { Currency } from './currency' +import { JsonObject, SerializedType } from './serialized-type' +import { Buffer } from 'buffer/' + +/** + * Interface for JSON objects that represent amounts + */ +interface IssueObject extends JsonObject { + currency: string + issuer?: string +} + +/** + * Type guard for AmountObject + */ +function isIssueObject(arg): arg is IssueObject { + const keys = Object.keys(arg).sort() + if (keys.length === 1) { + return keys[0] === 'currency' + } + return keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer' +} + +/** + * Class for serializing/Deserializing Amounts + */ +class Issue extends SerializedType { + static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(Buffer.alloc(20)) + + constructor(bytes: Buffer) { + super(bytes ?? Issue.ZERO_ISSUED_CURRENCY.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): Issue { + if (value instanceof Issue) { + return value + } + + if (isIssueObject(value)) { + const currency = Currency.from(value.currency).toBytes() + if (value.issuer == null) { + return new Issue(currency) + } + const issuer = AccountID.from(value.issuer).toBytes() + return new Issue(Buffer.concat([currency, issuer])) + } + + throw new Error('Invalid type to construct an Amount') + } + + /** + * Read an amount from a BinaryParser + * + * @param parser BinaryParser to read the Amount from + * @returns An Amount object + */ + static fromParser(parser: BinaryParser): Issue { + const currency = parser.read(20) + if (new Currency(currency).toJSON() === 'XRP') { + return new Issue(currency) + } + const currencyAndIssuer = [currency, parser.read(20)] + return new Issue(Buffer.concat(currencyAndIssuer)) + } + + /** + * Get the JSON representation of this Amount + * + * @returns the JSON interpretation of this.bytes + */ + toJSON(): IssueObject { + const parser = new BinaryParser(this.toString()) + const currency = Currency.fromParser(parser) as Currency + if (currency.toJSON() === 'XRP') { + return { currency: currency.toJSON() } + } + const issuer = AccountID.fromParser(parser) as AccountID + + return { + currency: currency.toJSON(), + issuer: issuer.toJSON(), + } + } +} + +export { Issue, IssueObject } diff --git a/packages/ripple-binary-codec/test/definitions.test.js b/packages/ripple-binary-codec/test/definitions.test.js index 5f6bb17fe4..cdea70f409 100644 --- a/packages/ripple-binary-codec/test/definitions.test.js +++ b/packages/ripple-binary-codec/test/definitions.test.js @@ -129,7 +129,7 @@ describe('encode and decode using new types as a parameter', function () { // Normally this would be generated directly from rippled with something like `server_definitions`. // Added here to make it easier to see what is actually changing in the definitions.json file. const definitions = JSON.parse(JSON.stringify(normalDefinitionsJson)) - definitions.TYPES.NewType = 24 + definitions.TYPES.NewType = 31 definitions.FIELDS.push([ 'TestField', { diff --git a/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json b/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json index 15aa20f198..f7995f77b1 100644 --- a/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json +++ b/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json @@ -4448,6 +4448,213 @@ "Flags": 0, "Sequence": 62 } + }, + { + "binary": "12002315000A2200000000240015DAE161400000000000271068400000000000000A6BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMCreate", + "TxnSignature": "B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E", + "Amount": "10000", + "Amount2": { + "currency": "ETH", + "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", + "value": "10000" + }, + "TradingFee": 10, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" + } + }, + { + "binary": "1200242200010000240015DAE168400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874408073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "TxnSignature": "8073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" + } + }, + { + "binary": "1200242200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744096CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB048114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "96CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB04" + } + }, + { + "binary": "1200242200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC1759018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC175901" + } + }, + { + "binary": "1200242200200000240015DAE16140000000000003E868400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C098114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C09" + } + }, + { + "binary": "1200242200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874405E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "5E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D" + } + }, + { + "binary": "1200252200010000240015DAE168400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874409D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E418028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "9D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E41802" + } + }, + { + "binary": "1200252200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D89491849079035018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D8949184907903501" + } + }, + { + "binary": "1200252200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA5607714952028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA560771495202" + } + }, + { + "binary": "1200252200200000240015DAE16140000000000003E868400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744042DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "42DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F" + } + }, + { + "binary": "1200252200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744045BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "45BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D" + } + }, + { + "binary": "1200272200000000240015DAE168400000000000000A6CD4C8E1BC9BF04000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0486DD4CC6F3B40B6C000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653AF019E01B81149A91957F8F16BC57F3F200CD8C98375BF1791586E1F10318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMBid", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "AuthAccounts": [{"AuthAccount": {"Account": "rEaHTti4HZsMBpxTAF4ncWxkcdqDh1h6P7"}}], + "BidMax": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "35"}, + "BidMin": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "25"}, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F" + } + }, + { + "binary": "1200261500EA2200000000240015DAE168400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B21078114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMVote", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "TradingFee": 234, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107" + } }], "ledgerData": [{ "binary": "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00", @@ -4463,4 +4670,4 @@ "transaction_hash": "DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F87" } }] -} \ No newline at end of file +} diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 3ad397fa39..9fc5446300 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -121,6 +121,9 @@ Wallet.fromMmnemonic() * `Wallet.fromMnemonic` detects when an invalid encoding is provided, and throws an error * Made unexpected errors in `submitAndWait` more verbose to make them easier to debug. +### Added +* Support for Automated Market Maker (AMM) transactions and requests as defined in XLS-30. + ## 2.3.1 (2022-06-27) ### Fixed * Signing tx with standard currency codes with lowercase and allowed symbols causing an error on decode. diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 69fd18a5b1..2d7e4e9c79 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -95,6 +95,9 @@ import { NFTInfoResponse, NFTHistoryRequest, NFTHistoryResponse, + // AMM methods + AMMInfoRequest, + AMMInfoResponse, } from '../models/methods' import { BaseRequest, BaseResponse } from '../models/methods/baseMethod' import { @@ -316,6 +319,7 @@ class Client extends EventEmitter { ): Promise public async request(r: AccountOffersRequest): Promise public async request(r: AccountTxRequest): Promise + public async request(r: AMMInfoRequest): Promise public async request(r: BookOffersRequest): Promise public async request(r: ChannelVerifyRequest): Promise public async request( diff --git a/packages/xrpl/src/models/common/index.ts b/packages/xrpl/src/models/common/index.ts index f64ae22f8a..8a4e06b655 100644 --- a/packages/xrpl/src/models/common/index.ts +++ b/packages/xrpl/src/models/common/index.ts @@ -141,3 +141,9 @@ export interface NFToken { nft_serial: number uri: string } + +export interface AuthAccount { + AuthAccount: { + account: string + } +} diff --git a/packages/xrpl/src/models/ledger/AMM.ts b/packages/xrpl/src/models/ledger/AMM.ts new file mode 100644 index 0000000000..48149711af --- /dev/null +++ b/packages/xrpl/src/models/ledger/AMM.ts @@ -0,0 +1,78 @@ +import { AuthAccount, Currency } from '../common' + +import BaseLedgerEntry from './BaseLedgerEntry' + +export interface VoteSlot { + VoteEntry: { + Account: string + TradingFee: number + VoteWeight: number + } +} + +/** + * The AMM object type describes a single Automated Market Maker (AMM) instance. + * + * @category Ledger Entries + */ +export default interface AMM extends BaseLedgerEntry { + LedgerEntryType: 'AMM' + /** + * The address of the special account that holds this AMM's assets. + */ + AMMAccount: string + /** + * The definition for one of the two assets this AMM holds. + */ + Asset: Currency + /** + * The definition for the other asset this AMM holds. + */ + Asset2: Currency + /** + * Details of the current owner of the auction slot. + */ + AuctionSlot?: { + /** + * The current owner of this auction slot. + */ + Account: string + /** + * A list of at most 4 additional accounts that are authorized to trade at the discounted fee for this AMM instance. + */ + AuthAccounts?: AuthAccount[] + /** + * The trading fee to be charged to the auction owner, in the same format as TradingFee. + * By default this is 0, meaning that the auction owner can trade at no fee instead of the standard fee for this AMM. + */ + DiscountedFee: number + /** + * The time when this slot expires, in seconds since the Ripple Epoch. + */ + Expiration: number + /** + * The amount the auction owner paid to win this slot, in LP Tokens. + */ + Price: Currency + } + /** + * The total outstanding balance of liquidity provider tokens from this AMM instance. + * The holders of these tokens can vote on the AMM's trading fee in proportion to their holdings, + * or redeem the tokens for a share of the AMM's assets which grows with the trading fees collected. + */ + LPTokenBalance: Currency + /** + * The percentage fee to be charged for trades against this AMM instance, in units of 1/100,000. + * The maximum value is 1000, for a 1% fee. + */ + TradingFee: number + /** + * A list of vote objects, representing votes on the pool's trading fee. + */ + VoteSlots?: VoteSlot[] + /** + * A bit-map of boolean flags. No flags are defined for the AMM object + * type, so this value is always 0. + */ + Flags: 0 +} diff --git a/packages/xrpl/src/models/ledger/AccountRoot.ts b/packages/xrpl/src/models/ledger/AccountRoot.ts index 87bde46815..a6d68bd9bb 100644 --- a/packages/xrpl/src/models/ledger/AccountRoot.ts +++ b/packages/xrpl/src/models/ledger/AccountRoot.ts @@ -126,6 +126,10 @@ export interface AccountRootFlagsInterface { * (It has DepositAuth enabled.) */ lsfDepositAuth?: boolean + /** + * This account is an Automated Market Maker (AMM) instance. + */ + lsfAMM?: boolean /** * Disallow incoming NFTOffers from other accounts. */ @@ -186,6 +190,10 @@ export enum AccountRootFlags { * (It has DepositAuth enabled.) */ lsfDepositAuth = 0x01000000, + /** + * This account is an Automated Market Maker (AMM) instance. + */ + lsfAMM = 0x02000000, /** * Disallow incoming NFTOffers from other accounts. */ diff --git a/packages/xrpl/src/models/ledger/LedgerEntry.ts b/packages/xrpl/src/models/ledger/LedgerEntry.ts index 1302c32e7d..c09883fb17 100644 --- a/packages/xrpl/src/models/ledger/LedgerEntry.ts +++ b/packages/xrpl/src/models/ledger/LedgerEntry.ts @@ -1,5 +1,6 @@ import AccountRoot from './AccountRoot' import Amendments from './Amendments' +import AMM from './AMM' import Check from './Check' import DepositPreauth from './DepositPreauth' import DirectoryNode from './DirectoryNode' @@ -16,6 +17,7 @@ import Ticket from './Ticket' type LedgerEntry = | AccountRoot | Amendments + | AMM | Check | DepositPreauth | DirectoryNode diff --git a/packages/xrpl/src/models/methods/ammInfo.ts b/packages/xrpl/src/models/methods/ammInfo.ts new file mode 100644 index 0000000000..bf907ffe43 --- /dev/null +++ b/packages/xrpl/src/models/methods/ammInfo.ts @@ -0,0 +1,145 @@ +import { Amount, Currency, IssuedCurrencyAmount } from '../common' + +import { BaseRequest, BaseResponse } from './baseMethod' + +/** + * The `amm_info` method gets information about an Automated Market Maker (AMM) instance. + * Returns an {@link AMMInfoResponse}. + * + * @category Requests + */ +export interface AMMInfoRequest extends BaseRequest { + command: 'amm_info' + + /** + * One of the assets of the AMM pool to look up. + */ + asset: Currency + + /** + * The other asset of the AMM pool. + */ + asset2: Currency +} + +/** + * Response expected from an {@link AMMInfoRequest}. + * + * @category Responses + */ +export interface AMMInfoResponse extends BaseResponse { + result: { + amm: { + /** + * The address of the AMM Account. + */ + account: string + + /** + * The total amount of one asset in the AMM's pool. + * (Note: This could be asset or asset2 from the request) + */ + amount: Amount + + /** + * The total amount of the other asset in the AMM's pool. + * (Note: This could be asset or asset2 from the request) + */ + amount2: Amount + + /** + * (Omitted for XRP) If true, the amount currency is currently frozen for asset. + */ + asset_frozen?: boolean + + /** + * (Omitted for XRP) If true, the amount currency is currently frozen for asset2. + */ + asset2_frozen?: boolean + + /** + * (May be omitted) An Auction Slot Object describing the current auction slot holder, if there is one. + */ + auction_slot?: { + /** + * The address of the account that owns the auction slot. + */ + account: string + + /** + * A list of additional accounts that the auction slot holder has designated as being eligible + * of the discounted trading fee. + * Each member of this array is an object with one field, account, containing the address of the designated account. + */ + auth_accounts: Array<{ + account: string + }> + + /** + * The discounted trading fee that applies to the auction slot holder, and any eligible accounts + * when trading against this AMM. + * This is always 0. + */ + discounted_fee: number + + /** + * The ISO 8601 UTC timestamp after which this auction slot expires. + * After expired, the auction slot does not apply (but the data can remain in the ledger + * until another transaction replaces it or cleans it up). + */ + expiration: string + + /** + * The amount, in LP Tokens, that the auction slot holder paid to win the auction slot. + * This affects the price to outbid the current slot holder. + */ + price: Amount + + /** + * The current 72-minute time interval this auction slot is in, from 0 to 19. + * The auction slot expires after 24 hours (20 intervals of 72 minutes) + * and affects the cost to outbid the current holder and how much the current holder is refunded if someone outbids them. + */ + time_interval: number + } + + /** + * The total amount of this AMM's LP Tokens outstanding. + */ + lp_token: IssuedCurrencyAmount + + /** + * The AMM's current trading fee, in units of 1/100,000; a value of 1 is equivalent to a 0.001% fee. + */ + trading_fee: number + + /** + * (May be omitted) The current votes for the AMM's trading fee, as Vote Slot Objects. + */ + vote_slots?: Array<{ + account: string + trading_fee: number + vote_weight: number + }> + } + + /** + * The identifying hash of the ledger that was used to generate this + * response. + */ + ledger_hash?: string + + /** + * The ledger index of the ledger version that was used to generate this + * response. + */ + ledger_index?: number + + /** + * If included and set to true, the information in this response comes from + * a validated ledger version. Otherwise, the information is subject to + * change. + */ + validated?: boolean + } +} diff --git a/packages/xrpl/src/models/methods/index.ts b/packages/xrpl/src/models/methods/index.ts index e8ca36924f..530e7e75cf 100644 --- a/packages/xrpl/src/models/methods/index.ts +++ b/packages/xrpl/src/models/methods/index.ts @@ -41,6 +41,7 @@ import { AccountTxResponse, AccountTxTransaction, } from './accountTx' +import { AMMInfoRequest, AMMInfoResponse } from './ammInfo' import { BaseRequest, BaseResponse, @@ -196,6 +197,8 @@ type Request = // clio only methods | NFTInfoRequest | NFTHistoryRequest + // AMM methods + | AMMInfoRequest /** * @category Responses @@ -247,6 +250,8 @@ type Response = // clio only methods | NFTInfoResponse | NFTHistoryResponse + // AMM methods + | AMMInfoResponse export { // Allow users to define their own requests and responses. This is useful for releasing experimental versions @@ -380,4 +385,7 @@ export { NFTHistoryRequest, NFTHistoryResponse, NFTHistoryTransaction, + // AMM methods + AMMInfoRequest, + AMMInfoResponse, } diff --git a/packages/xrpl/src/models/methods/ledgerEntry.ts b/packages/xrpl/src/models/methods/ledgerEntry.ts index fd792cc3b3..acd44d69de 100644 --- a/packages/xrpl/src/models/methods/ledgerEntry.ts +++ b/packages/xrpl/src/models/methods/ledgerEntry.ts @@ -20,6 +20,21 @@ import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod' */ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest { command: 'ledger_entry' + /** + * Retrieve an Automated Market Maker (AMM) object from the ledger. + * This is similar to amm_info method, but the ledger_entry version returns only the ledger entry as stored. + */ + amm?: { + asset: { + currency: string + issuer?: string + } + asset2: { + currency: string + issuer?: string + } + } + /** * If true, return the requested ledger object's contents as a hex string in * the XRP Ledger's binary format. Otherwise, return data in JSON format. The diff --git a/packages/xrpl/src/models/transactions/AMMBid.ts b/packages/xrpl/src/models/transactions/AMMBid.ts new file mode 100644 index 0000000000..b7634ba506 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMBid.ts @@ -0,0 +1,140 @@ +/* eslint-disable complexity -- required for validateAMMBid */ +import { ValidationError } from '../../errors' +import { Amount, AuthAccount, Currency } from '../common' + +import { + BaseTransaction, + isAmount, + isCurrency, + validateBaseTransaction, +} from './common' + +const MAX_AUTH_ACCOUNTS = 4 + +/** + * Bid on an Automated Market Maker's (AMM's) auction slot. + * + * If you win, you can trade against the AMM at a discounted fee until you are outbid or 24 hours have passed. + * If you are outbid before 24 hours have passed, you are refunded part of the cost of your bid based on how much time remains. + * You bid using the AMM's LP Tokens; the amount of a winning bid is returned to the AMM, + * decreasing the outstanding balance of LP Tokens. + */ +export interface AMMBid extends BaseTransaction { + TransactionType: 'AMMBid' + + /** + * The definition for one of the assets in the AMM's pool. + */ + Asset: Currency + + /** + * The definition for the other asset in the AMM's pool. + */ + Asset2: Currency + + /** + * Pay at least this amount for the slot. + * Setting this value higher makes it harder for others to outbid you. + * If omitted, pay the minimum necessary to win the bid. + */ + BidMin?: Amount + + /** + * Pay at most this amount for the slot. + * If the cost to win the bid is higher than this amount, the transaction fails. + * If omitted, pay as much as necessary to win the bid. + */ + BidMax?: Amount + + /** + * A list of up to 4 additional accounts that you allow to trade at the discounted fee. + * This cannot include the address of the transaction sender. + */ + AuthAccounts?: AuthAccount[] +} + +/** + * Verify the form and type of an AMMBid at runtime. + * + * @param tx - An AMMBid Transaction. + * @throws When the AMMBid is Malformed. + */ +export function validateAMMBid(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Asset == null) { + throw new ValidationError('AMMBid: missing field Asset') + } + + if (!isCurrency(tx.Asset)) { + throw new ValidationError('AMMBid: Asset must be a Currency') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMBid: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMBid: Asset2 must be a Currency') + } + + if (tx.BidMin != null && !isAmount(tx.BidMin)) { + throw new ValidationError('AMMBid: BidMin must be an Amount') + } + + if (tx.BidMax != null && !isAmount(tx.BidMax)) { + throw new ValidationError('AMMBid: BidMax must be an Amount') + } + + if (tx.AuthAccounts != null) { + if (!Array.isArray(tx.AuthAccounts)) { + throw new ValidationError( + `AMMBid: AuthAccounts must be an AuthAccount array`, + ) + } + if (tx.AuthAccounts.length > MAX_AUTH_ACCOUNTS) { + throw new ValidationError( + `AMMBid: AuthAccounts length must not be greater than ${MAX_AUTH_ACCOUNTS}`, + ) + } + validateAuthAccounts( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS + tx.Account as string, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS + tx.AuthAccounts as Array>, + ) + } +} + +function validateAuthAccounts( + senderAddress: string, + authAccounts: Array>, +): boolean { + for (const authAccount of authAccounts) { + if ( + authAccount.AuthAccount == null || + typeof authAccount.AuthAccount !== 'object' + ) { + throw new ValidationError(`AMMBid: invalid AuthAccounts`) + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- used for null check + // @ts-expect-error -- used for null check + if (authAccount.AuthAccount.Account == null) { + throw new ValidationError(`AMMBid: invalid AuthAccounts`) + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- used for null check + // @ts-expect-error -- used for null check + if (typeof authAccount.AuthAccount.Account !== 'string') { + throw new ValidationError(`AMMBid: invalid AuthAccounts`) + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- used for null check + // @ts-expect-error -- used for null check + if (authAccount.AuthAccount.Account === senderAddress) { + throw new ValidationError( + `AMMBid: AuthAccounts must not include sender's address`, + ) + } + } + + return true +} diff --git a/packages/xrpl/src/models/transactions/AMMCreate.ts b/packages/xrpl/src/models/transactions/AMMCreate.ts new file mode 100644 index 0000000000..8924fa75ff --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMCreate.ts @@ -0,0 +1,80 @@ +import { ValidationError } from '../../errors' +import { Amount } from '../common' + +import { BaseTransaction, isAmount, validateBaseTransaction } from './common' + +export const AMM_MAX_TRADING_FEE = 1000 + +/** + * Create a new Automated Market Maker (AMM) instance for trading a pair of assets (fungible tokens or XRP). + * + * Creates both an AMM object and a special AccountRoot object to represent the AMM. + * Also transfers ownership of the starting balance of both assets from the sender to the created AccountRoot + * and issues an initial balance of liquidity provider tokens (LP Tokens) from the AMM account to the sender. + * + * CAUTION: When you create the AMM, you should fund it with (approximately) equal-value amounts of each asset. + * Otherwise, other users can profit at your expense by trading with this AMM (performing arbitrage). + * The currency risk that liquidity providers take on increases with the volatility (potential for imbalance) of the asset pair. + * The higher the trading fee, the more it offsets this risk, + * so it's best to set the trading fee based on the volatility of the asset pair. + */ +export interface AMMCreate extends BaseTransaction { + TransactionType: 'AMMCreate' + + /** + * The first of the two assets to fund this AMM with. This must be a positive amount. + */ + Amount: Amount + + /** + * The second of the two assets to fund this AMM with. This must be a positive amount. + */ + Amount2: Amount + + /** + * The fee to charge for trades against this AMM instance, in units of 1/100,000; a value of 1 is equivalent to 0.001%. + * The maximum value is 1000, indicating a 1% fee. + * The minimum value is 0. + */ + TradingFee: number +} + +/** + * Verify the form and type of an AMMCreate at runtime. + * + * @param tx - An AMMCreate Transaction. + * @throws When the AMMCreate is Malformed. + */ +export function validateAMMCreate(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Amount == null) { + throw new ValidationError('AMMCreate: missing field Amount') + } + + if (!isAmount(tx.Amount)) { + throw new ValidationError('AMMCreate: Amount must be an Amount') + } + + if (tx.Amount2 == null) { + throw new ValidationError('AMMCreate: missing field Amount2') + } + + if (!isAmount(tx.Amount2)) { + throw new ValidationError('AMMCreate: Amount2 must be an Amount') + } + + if (tx.TradingFee == null) { + throw new ValidationError('AMMCreate: missing field TradingFee') + } + + if (typeof tx.TradingFee !== 'number') { + throw new ValidationError('AMMCreate: TradingFee must be a number') + } + + if (tx.TradingFee < 0 || tx.TradingFee > AMM_MAX_TRADING_FEE) { + throw new ValidationError( + `AMMCreate: TradingFee must be between 0 and ${AMM_MAX_TRADING_FEE}`, + ) + } +} diff --git a/packages/xrpl/src/models/transactions/AMMDelete.ts b/packages/xrpl/src/models/transactions/AMMDelete.ts new file mode 100644 index 0000000000..6e64f8c58b --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMDelete.ts @@ -0,0 +1,55 @@ +import { ValidationError } from '../../errors' +import { Currency } from '../common' + +import { BaseTransaction, isCurrency, validateBaseTransaction } from './common' + +/** + * Delete an empty Automated Market Maker (AMM) instance that could not be fully deleted automatically. + * + * Tip: The AMMWithdraw transaction automatically tries to delete an AMM, along with associated ledger + * entries such as empty trust lines, if it withdrew all the assets from the AMM's pool. + * However, if there are too many trust lines to the AMM account to remove in one transaction, + * it may stop before fully removing the AMM. Similarly, an AMMDelete transaction removes up to + * a maximum number of trust lines; in extreme cases, it may take several AMMDelete transactions + * to fully delete the trust lines and the associated AMM. + * In all cases, the AMM ledger entry and AMM account are deleted by the last such transaction. + */ +export interface AMMDelete extends BaseTransaction { + TransactionType: 'AMMDelete' + + /** + * The definition for one of the assets in the AMM's pool. + */ + Asset: Currency + + /** + * The definition for the other asset in the AMM's pool. + */ + Asset2: Currency +} + +/** + * Verify the form and type of an AMMDelete at runtime. + * + * @param tx - An AMMDelete Transaction. + * @throws When the AMMDelete is Malformed. + */ +export function validateAMMDelete(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Asset == null) { + throw new ValidationError('AMMDelete: missing field Asset') + } + + if (!isCurrency(tx.Asset)) { + throw new ValidationError('AMMDelete: Asset must be a Currency') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMDelete: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMDelete: Asset2 must be a Currency') + } +} diff --git a/packages/xrpl/src/models/transactions/AMMDeposit.ts b/packages/xrpl/src/models/transactions/AMMDeposit.ts new file mode 100644 index 0000000000..92648fba50 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMDeposit.ts @@ -0,0 +1,130 @@ +/* eslint-disable complexity -- required for validateAMMDeposit */ +import { ValidationError } from '../../errors' +import { Amount, Currency, IssuedCurrencyAmount } from '../common' + +import { + BaseTransaction, + GlobalFlags, + isAmount, + isCurrency, + isIssuedCurrency, + validateBaseTransaction, +} from './common' + +/** + * Enum representing values for AMMDeposit Transaction Flags. + * + * @category Transaction Flags + */ +export enum AMMDepositFlags { + tfLPToken = 0x00010000, + tfSingleAsset = 0x00080000, + tfTwoAsset = 0x00100000, + tfOneAssetLPToken = 0x00200000, + tfLimitLPToken = 0x00400000, +} + +export interface AMMDepositFlagsInterface extends GlobalFlags { + tfLPToken?: boolean + tfSingleAsset?: boolean + tfTwoAsset?: boolean + tfOneAssetLPToken?: boolean + tfLimitLPToken?: boolean +} + +/** + * Deposit funds into an Automated Market Maker (AMM) instance + * and receive the AMM's liquidity provider tokens (LP Tokens) in exchange. + * + * You can deposit one or both of the assets in the AMM's pool. + * If successful, this transaction creates a trust line to the AMM Account (limit 0) to hold the LP Tokens. + */ +export interface AMMDeposit extends BaseTransaction { + TransactionType: 'AMMDeposit' + + /** + * The definition for one of the assets in the AMM's pool. + */ + Asset: Currency + + /** + * The definition for the other asset in the AMM's pool. + */ + Asset2: Currency + + /** + * The amount of one asset to deposit to the AMM. + * If present, this must match the type of one of the assets (tokens or XRP) in the AMM's pool. + */ + Amount?: Amount + + /** + * The amount of another asset to add to the AMM. + * If present, this must match the type of the other asset in the AMM's pool and cannot be the same asset as Amount. + */ + Amount2?: Amount + + /** + * The maximum effective price, in the deposit asset, to pay for each LP Token received. + */ + EPrice?: Amount + + /** + * How many of the AMM's LP Tokens to buy. + */ + LPTokenOut?: IssuedCurrencyAmount +} + +/** + * Verify the form and type of an AMMDeposit at runtime. + * + * @param tx - An AMMDeposit Transaction. + * @throws When the AMMDeposit is Malformed. + */ +export function validateAMMDeposit(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Asset == null) { + throw new ValidationError('AMMDeposit: missing field Asset') + } + + if (!isCurrency(tx.Asset)) { + throw new ValidationError('AMMDeposit: Asset must be a Currency') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMDeposit: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMDeposit: Asset2 must be a Currency') + } + + if (tx.Amount2 != null && tx.Amount == null) { + throw new ValidationError('AMMDeposit: must set Amount with Amount2') + } else if (tx.EPrice != null && tx.Amount == null) { + throw new ValidationError('AMMDeposit: must set Amount with EPrice') + } else if (tx.LPTokenOut == null && tx.Amount == null) { + throw new ValidationError( + 'AMMDeposit: must set at least LPTokenOut or Amount', + ) + } + + if (tx.LPTokenOut != null && !isIssuedCurrency(tx.LPTokenOut)) { + throw new ValidationError( + 'AMMDeposit: LPTokenOut must be an IssuedCurrencyAmount', + ) + } + + if (tx.Amount != null && !isAmount(tx.Amount)) { + throw new ValidationError('AMMDeposit: Amount must be an Amount') + } + + if (tx.Amount2 != null && !isAmount(tx.Amount2)) { + throw new ValidationError('AMMDeposit: Amount2 must be an Amount') + } + + if (tx.EPrice != null && !isAmount(tx.EPrice)) { + throw new ValidationError('AMMDeposit: EPrice must be an Amount') + } +} diff --git a/packages/xrpl/src/models/transactions/AMMVote.ts b/packages/xrpl/src/models/transactions/AMMVote.ts new file mode 100644 index 0000000000..0d469fa0a8 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMVote.ts @@ -0,0 +1,71 @@ +import { ValidationError } from '../../errors' +import { Currency } from '../common' + +import { AMM_MAX_TRADING_FEE } from './AMMCreate' +import { BaseTransaction, isCurrency, validateBaseTransaction } from './common' + +/** + * Vote on the trading fee for an Automated Market Maker (AMM) instance. + * + * Up to 8 accounts can vote in proportion to the amount of the AMM's LP Tokens they hold. + * Each new vote re-calculates the AMM's trading fee based on a weighted average of the votes. + */ +export interface AMMVote extends BaseTransaction { + TransactionType: 'AMMVote' + + /** + * The definition for one of the assets in the AMM's pool. + */ + Asset: Currency + + /** + * The definition for the other asset in the AMM's pool. + */ + Asset2: Currency + + /** + * The proposed fee to vote for, in units of 1/100,000; a value of 1 is equivalent to 0.001%. + * The maximum value is 1000, indicating a 1% fee. + */ + TradingFee: number +} + +/** + * Verify the form and type of an AMMVote at runtime. + * + * @param tx - An AMMVote Transaction. + * @throws When the AMMVote is Malformed. + */ +export function validateAMMVote(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Asset == null) { + throw new ValidationError('AMMVote: missing field Asset') + } + + if (!isCurrency(tx.Asset)) { + throw new ValidationError('AMMVote: Asset must be a Currency') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMVote: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMVote: Asset2 must be a Currency') + } + + if (tx.TradingFee == null) { + throw new ValidationError('AMMVote: missing field TradingFee') + } + + if (typeof tx.TradingFee !== 'number') { + throw new ValidationError('AMMVote: TradingFee must be a number') + } + + if (tx.TradingFee < 0 || tx.TradingFee > AMM_MAX_TRADING_FEE) { + throw new ValidationError( + `AMMVote: TradingFee must be between 0 and ${AMM_MAX_TRADING_FEE}`, + ) + } +} diff --git a/packages/xrpl/src/models/transactions/AMMWithdraw.ts b/packages/xrpl/src/models/transactions/AMMWithdraw.ts new file mode 100644 index 0000000000..77411616bc --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMWithdraw.ts @@ -0,0 +1,126 @@ +/* eslint-disable complexity -- required for validateAMMWithdraw */ +import { ValidationError } from '../../errors' +import { Amount, Currency, IssuedCurrencyAmount } from '../common' + +import { + BaseTransaction, + GlobalFlags, + isAmount, + isCurrency, + isIssuedCurrency, + validateBaseTransaction, +} from './common' + +/** + * Enum representing values for AMMWithdrawFlags Transaction Flags. + * + * @category Transaction Flags + */ +export enum AMMWithdrawFlags { + tfLPToken = 0x00010000, + tfWithdrawAll = 0x00020000, + tfOneAssetWithdrawAll = 0x00040000, + tfSingleAsset = 0x00080000, + tfTwoAsset = 0x00100000, + tfOneAssetLPToken = 0x00200000, + tfLimitLPToken = 0x00400000, +} + +export interface AMMWithdrawFlagsInterface extends GlobalFlags { + tfLPToken?: boolean + tfWithdrawAll?: boolean + tfOneAssetWithdrawAll?: boolean + tfSingleAsset?: boolean + tfTwoAsset?: boolean + tfOneAssetLPToken?: boolean + tfLimitLPToken?: boolean +} + +/** + * Withdraw assets from an Automated Market Maker (AMM) instance by returning the AMM's liquidity provider tokens (LP Tokens). + */ +export interface AMMWithdraw extends BaseTransaction { + TransactionType: 'AMMWithdraw' + + /** + * The definition for one of the assets in the AMM's pool. + */ + Asset: Currency + + /** + * The definition for the other asset in the AMM's pool. + */ + Asset2: Currency + + /** + * The amount of one asset to withdraw from the AMM. + * This must match the type of one of the assets (tokens or XRP) in the AMM's pool. + */ + Amount?: Amount + + /** + * The amount of another asset to withdraw from the AMM. + * If present, this must match the type of the other asset in the AMM's pool and cannot be the same type as Amount. + */ + Amount2?: Amount + + /** + * The minimum effective price, in LP Token returned, to pay per unit of the asset to withdraw. + */ + EPrice?: Amount + + /** + * How many of the AMM's LP Tokens to redeem. + */ + LPTokenIn?: IssuedCurrencyAmount +} + +/** + * Verify the form and type of an AMMWithdraw at runtime. + * + * @param tx - An AMMWithdraw Transaction. + * @throws When the AMMWithdraw is Malformed. + */ +export function validateAMMWithdraw(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Asset == null) { + throw new ValidationError('AMMWithdraw: missing field Asset') + } + + if (!isCurrency(tx.Asset)) { + throw new ValidationError('AMMWithdraw: Asset must be a Currency') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMWithdraw: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMWithdraw: Asset2 must be a Currency') + } + + if (tx.Amount2 != null && tx.Amount == null) { + throw new ValidationError('AMMWithdraw: must set Amount with Amount2') + } else if (tx.EPrice != null && tx.Amount == null) { + throw new ValidationError('AMMWithdraw: must set Amount with EPrice') + } + + if (tx.LPTokenIn != null && !isIssuedCurrency(tx.LPTokenIn)) { + throw new ValidationError( + 'AMMWithdraw: LPTokenIn must be an IssuedCurrencyAmount', + ) + } + + if (tx.Amount != null && !isAmount(tx.Amount)) { + throw new ValidationError('AMMWithdraw: Amount must be an Amount') + } + + if (tx.Amount2 != null && !isAmount(tx.Amount2)) { + throw new ValidationError('AMMWithdraw: Amount2 must be an Amount') + } + + if (tx.EPrice != null && !isAmount(tx.EPrice)) { + throw new ValidationError('AMMWithdraw: EPrice must be an Amount') + } +} diff --git a/packages/xrpl/src/models/transactions/common.ts b/packages/xrpl/src/models/transactions/common.ts index cbb7d4a49f..f30e390bec 100644 --- a/packages/xrpl/src/models/transactions/common.ts +++ b/packages/xrpl/src/models/transactions/common.ts @@ -4,7 +4,7 @@ import { TRANSACTION_TYPES } from 'ripple-binary-codec' import { ValidationError } from '../../errors' -import { Amount, IssuedCurrencyAmount, Memo, Signer } from '../common' +import { Amount, Currency, IssuedCurrencyAmount, Memo, Signer } from '../common' import { onlyHasFields } from '../utils' const MEMO_SIZE = 3 @@ -50,17 +50,36 @@ function isSigner(obj: unknown): boolean { ) } +const XRP_CURRENCY_SIZE = 1 +const ISSUE_SIZE = 2 const ISSUED_CURRENCY_SIZE = 3 function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' } +/** + * Verify the form and type of an IssuedCurrency at runtime. + * + * @param input - The input to check the form and type of. + * @returns Whether the IssuedCurrency is properly formed. + */ +export function isCurrency(input: unknown): input is Currency { + return ( + isRecord(input) && + ((Object.keys(input).length === ISSUE_SIZE && + typeof input.issuer === 'string' && + typeof input.currency === 'string') || + (Object.keys(input).length === XRP_CURRENCY_SIZE && + input.currency === 'XRP')) + ) +} + /** * Verify the form and type of an IssuedCurrencyAmount at runtime. * * @param input - The input to check the form and type of. - * @returns Whether the IssuedCurrencyAmount is malformed. + * @returns Whether the IssuedCurrencyAmount is properly formed. */ export function isIssuedCurrency( input: unknown, @@ -78,7 +97,7 @@ export function isIssuedCurrency( * Verify the form and type of an Amount at runtime. * * @param amount - The object to check the form and type of. - * @returns Whether the Amount is malformed. + * @returns Whether the Amount is properly formed. */ export function isAmount(amount: unknown): amount is Amount { return typeof amount === 'string' || isIssuedCurrency(amount) diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index c817df3367..f4ac17162b 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -8,6 +8,20 @@ export { AccountSet, } from './accountSet' export { AccountDelete } from './accountDelete' +export { AMMBid } from './AMMBid' +export { AMMDelete } from './AMMDelete' +export { + AMMDepositFlags, + AMMDepositFlagsInterface, + AMMDeposit, +} from './AMMDeposit' +export { AMMCreate } from './AMMCreate' +export { AMMVote } from './AMMVote' +export { + AMMWithdrawFlags, + AMMWithdrawFlagsInterface, + AMMWithdraw, +} from './AMMWithdraw' export { CheckCancel } from './checkCancel' export { CheckCash } from './checkCash' export { CheckCreate } from './checkCreate' diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index aee51a679e..d726e9962d 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -8,6 +8,12 @@ import { setTransactionFlagsToNumber } from '../utils/flags' import { AccountDelete, validateAccountDelete } from './accountDelete' import { AccountSet, validateAccountSet } from './accountSet' +import { AMMBid, validateAMMBid } from './AMMBid' +import { AMMCreate, validateAMMCreate } from './AMMCreate' +import { AMMDelete, validateAMMDelete } from './AMMDelete' +import { AMMDeposit, validateAMMDeposit } from './AMMDeposit' +import { AMMVote, validateAMMVote } from './AMMVote' +import { AMMWithdraw, validateAMMWithdraw } from './AMMWithdraw' import { CheckCancel, validateCheckCancel } from './checkCancel' import { CheckCash, validateCheckCash } from './checkCash' import { CheckCreate, validateCheckCreate } from './checkCreate' @@ -58,6 +64,12 @@ import { TrustSet, validateTrustSet } from './trustSet' export type Transaction = | AccountDelete | AccountSet + | AMMBid + | AMMDelete + | AMMDeposit + | AMMCreate + | AMMVote + | AMMWithdraw | CheckCancel | CheckCash | CheckCreate @@ -167,6 +179,30 @@ export function validate(transaction: Record): void { validateAccountSet(tx) break + case 'AMMBid': + validateAMMBid(tx) + break + + case 'AMMDelete': + validateAMMDelete(tx) + break + + case 'AMMDeposit': + validateAMMDeposit(tx) + break + + case 'AMMCreate': + validateAMMCreate(tx) + break + + case 'AMMVote': + validateAMMVote(tx) + break + + case 'AMMWithdraw': + validateAMMWithdraw(tx) + break + case 'CheckCancel': validateCheckCancel(tx) break diff --git a/packages/xrpl/src/models/utils/flags.ts b/packages/xrpl/src/models/utils/flags.ts index 5f11205778..8f7169ad60 100644 --- a/packages/xrpl/src/models/utils/flags.ts +++ b/packages/xrpl/src/models/utils/flags.ts @@ -6,22 +6,15 @@ import { AccountRootFlagsInterface, AccountRootFlags, } from '../ledger/AccountRoot' -import { - AccountSetFlagsInterface, - AccountSetTfFlags, -} from '../transactions/accountSet' +import { AccountSetTfFlags } from '../transactions/accountSet' +import { AMMDepositFlags } from '../transactions/AMMDeposit' +import { AMMWithdrawFlags } from '../transactions/AMMWithdraw' import { GlobalFlags } from '../transactions/common' -import { - OfferCreateFlagsInterface, - OfferCreateFlags, -} from '../transactions/offerCreate' -import { PaymentFlagsInterface, PaymentFlags } from '../transactions/payment' -import { - PaymentChannelClaimFlagsInterface, - PaymentChannelClaimFlags, -} from '../transactions/paymentChannelClaim' +import { OfferCreateFlags } from '../transactions/offerCreate' +import { PaymentFlags } from '../transactions/payment' +import { PaymentChannelClaimFlags } from '../transactions/paymentChannelClaim' import type { Transaction } from '../transactions/transaction' -import { TrustSetFlagsInterface, TrustSetFlags } from '../transactions/trustSet' +import { TrustSetFlags } from '../transactions/trustSet' import { isFlagEnabled } from '.' @@ -65,55 +58,33 @@ export function setTransactionFlagsToNumber(tx: Transaction): void { switch (tx.TransactionType) { case 'AccountSet': - tx.Flags = convertAccountSetFlagsToNumber(tx.Flags) + tx.Flags = convertFlagsToNumber(tx.Flags, AccountSetTfFlags) + return + case 'AMMDeposit': + tx.Flags = convertFlagsToNumber(tx.Flags, AMMDepositFlags) + return + case 'AMMWithdraw': + tx.Flags = convertFlagsToNumber(tx.Flags, AMMWithdrawFlags) return case 'OfferCreate': - tx.Flags = convertOfferCreateFlagsToNumber(tx.Flags) + tx.Flags = convertFlagsToNumber(tx.Flags, OfferCreateFlags) return case 'PaymentChannelClaim': - tx.Flags = convertPaymentChannelClaimFlagsToNumber(tx.Flags) + tx.Flags = convertFlagsToNumber(tx.Flags, PaymentChannelClaimFlags) return case 'Payment': - tx.Flags = convertPaymentTransactionFlagsToNumber(tx.Flags) + tx.Flags = convertFlagsToNumber(tx.Flags, PaymentFlags) return case 'TrustSet': - tx.Flags = convertTrustSetFlagsToNumber(tx.Flags) + tx.Flags = convertFlagsToNumber(tx.Flags, TrustSetFlags) return default: tx.Flags = 0 } } -function convertAccountSetFlagsToNumber( - flags: AccountSetFlagsInterface, -): number { - return reduceFlags(flags, AccountSetTfFlags) -} - -function convertOfferCreateFlagsToNumber( - flags: OfferCreateFlagsInterface, -): number { - return reduceFlags(flags, OfferCreateFlags) -} - -function convertPaymentChannelClaimFlagsToNumber( - flags: PaymentChannelClaimFlagsInterface, -): number { - return reduceFlags(flags, PaymentChannelClaimFlags) -} - -function convertPaymentTransactionFlagsToNumber( - flags: PaymentFlagsInterface, -): number { - return reduceFlags(flags, PaymentFlags) -} - -function convertTrustSetFlagsToNumber(flags: TrustSetFlagsInterface): number { - return reduceFlags(flags, TrustSetFlags) -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- added ValidationError check for flagEnum -function reduceFlags(flags: GlobalFlags, flagEnum: any): number { +function convertFlagsToNumber(flags: GlobalFlags, flagEnum: any): number { return Object.keys(flags).reduce((resultFlags, flag) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access if (flagEnum[flag] == null) { diff --git a/packages/xrpl/src/sugar/autofill.ts b/packages/xrpl/src/sugar/autofill.ts index 82766166c3..aa4c08005a 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -275,7 +275,7 @@ async function setNextValidSequenceNumber( tx.Sequence = data.result.account_data.Sequence } -async function fetchAccountDeleteFee(client: Client): Promise { +async function fetchOwnerReserveFee(client: Client): Promise { const response = await client.request({ command: 'server_state' }) const fee = response.result.state.validated_ledger?.reserve_inc @@ -307,9 +307,11 @@ async function calculateFeePerTransactionType( baseFee = product.dp(0, BigNumber.ROUND_CEIL) } - // AccountDelete Transaction - if (tx.TransactionType === 'AccountDelete') { - baseFee = await fetchAccountDeleteFee(client) + if ( + tx.TransactionType === 'AccountDelete' || + tx.TransactionType === 'AMMCreate' + ) { + baseFee = await fetchOwnerReserveFee(client) } /* diff --git a/packages/xrpl/test/models/AMMBid.test.ts b/packages/xrpl/test/models/AMMBid.test.ts new file mode 100644 index 0000000000..c958804a80 --- /dev/null +++ b/packages/xrpl/test/models/AMMBid.test.ts @@ -0,0 +1,168 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateAMMBid } from '../../src/models/transactions/AMMBid' + +/** + * AMMBid Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMBid', function () { + let bid + + beforeEach(function () { + bid = { + TransactionType: 'AMMBid', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Asset: { + currency: 'XRP', + }, + Asset2: { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + }, + BidMin: '5', + BidMax: '10', + AuthAccounts: [ + { + AuthAccount: { + Account: 'rNZdsTBP5tH1M6GHC6bTreHAp6ouP8iZSh', + }, + }, + { + AuthAccount: { + Account: 'rfpFv97Dwu89FTyUwPjtpZBbuZxTqqgTmH', + }, + }, + { + AuthAccount: { + Account: 'rzzYHPGb8Pa64oqxCzmuffm122bitq3Vb', + }, + }, + { + AuthAccount: { + Account: 'rhwxHxaHok86fe4LykBom1jSJ3RYQJs1h4', + }, + }, + ], + Sequence: 1337, + } as any + }) + + it(`verifies valid AMMBid`, function () { + assert.doesNotThrow(() => validateAMMBid(bid)) + assert.doesNotThrow(() => validate(bid)) + }) + + it(`throws w/ missing field Asset`, function () { + delete bid.Asset + const errorMessage = 'AMMBid: missing field Asset' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ Asset must be a Currency`, function () { + bid.Asset = 1234 + const errorMessage = 'AMMBid: Asset must be a Currency' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ missing field Asset2`, function () { + delete bid.Asset2 + const errorMessage = 'AMMBid: missing field Asset2' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ Asset2 must be a Currency`, function () { + bid.Asset2 = 1234 + const errorMessage = 'AMMBid: Asset2 must be a Currency' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ BidMin must be an Amount`, function () { + bid.BidMin = 5 + const errorMessage = 'AMMBid: BidMin must be an Amount' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ BidMax must be an Amount`, function () { + bid.BidMax = 10 + const errorMessage = 'AMMBid: BidMax must be an Amount' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ AuthAccounts length must not be greater than 4`, function () { + bid.AuthAccounts.push({ + AuthAccount: { + Account: 'r3X6noRsvaLapAKCG78zAtWcbhB3sggS1s', + }, + }) + const errorMessage = + 'AMMBid: AuthAccounts length must not be greater than 4' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ AuthAccounts must be an AuthAccount array`, function () { + bid.AuthAccounts = 1234 + const errorMessage = 'AMMBid: AuthAccounts must be an AuthAccount array' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ invalid AuthAccounts when AuthAccount is null`, function () { + bid.AuthAccounts[0] = { + AuthAccount: null, + } + const errorMessage = 'AMMBid: invalid AuthAccounts' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ invalid AuthAccounts when AuthAccount is undefined`, function () { + bid.AuthAccounts[0] = { + AuthAccount: undefined, + } + const errorMessage = 'AMMBid: invalid AuthAccounts' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ invalid AuthAccounts when AuthAccount is not an object`, function () { + bid.AuthAccounts[0] = { + AuthAccount: 1234, + } + const errorMessage = 'AMMBid: invalid AuthAccounts' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ invalid AuthAccounts when AuthAccount.Account is not a string`, function () { + bid.AuthAccounts[0] = { + AuthAccount: { + Account: 1234, + }, + } + const errorMessage = 'AMMBid: invalid AuthAccounts' + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) + + it(`throws w/ AuthAccounts must not include sender's address`, function () { + bid.AuthAccounts[0] = { + AuthAccount: { + Account: bid.Account, + }, + } + const errorMessage = + "AMMBid: AuthAccounts must not include sender's address" + assert.throws(() => validateAMMBid(bid), ValidationError, errorMessage) + assert.throws(() => validate(bid), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/AMMCreate.test.ts b/packages/xrpl/test/models/AMMCreate.test.ts new file mode 100644 index 0000000000..56242140ab --- /dev/null +++ b/packages/xrpl/test/models/AMMCreate.test.ts @@ -0,0 +1,121 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateAMMCreate } from '../../src/models/transactions/AMMCreate' + +/** + * AMMCreate Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMCreate', function () { + let ammCreate + + beforeEach(function () { + ammCreate = { + TransactionType: 'AMMCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Amount: '1000', + Amount2: { + currency: 'USD', + issuer: 'rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9', + value: '1000', + }, + TradingFee: 12, + Sequence: 1337, + } as any + }) + + it(`verifies valid AMMCreate`, function () { + assert.doesNotThrow(() => validateAMMCreate(ammCreate)) + assert.doesNotThrow(() => validate(ammCreate)) + }) + + it(`throws w/ missing Amount`, function () { + delete ammCreate.Amount + const errorMessage = 'AMMCreate: missing field Amount' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws w/ Amount must be an Amount`, function () { + ammCreate.Amount = 1000 + const errorMessage = 'AMMCreate: Amount must be an Amount' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws w/ missing Amount2`, function () { + delete ammCreate.Amount2 + const errorMessage = 'AMMCreate: missing field Amount2' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + ammCreate.Amount2 = 1000 + const errorMessage = 'AMMCreate: Amount2 must be an Amount' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws w/ missing TradingFee`, function () { + delete ammCreate.TradingFee + const errorMessage = 'AMMCreate: missing field TradingFee' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws w/ TradingFee must be a number`, function () { + ammCreate.TradingFee = '12' + const errorMessage = 'AMMCreate: TradingFee must be a number' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws when TradingFee is greater than 1000`, function () { + ammCreate.TradingFee = 1001 + const errorMessage = 'AMMCreate: TradingFee must be between 0 and 1000' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) + + it(`throws when TradingFee is a negative number`, function () { + ammCreate.TradingFee = -1 + const errorMessage = 'AMMCreate: TradingFee must be between 0 and 1000' + assert.throws( + () => validateAMMCreate(ammCreate), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammCreate), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/AMMDelete.test.ts b/packages/xrpl/test/models/AMMDelete.test.ts new file mode 100644 index 0000000000..8c4c9936c8 --- /dev/null +++ b/packages/xrpl/test/models/AMMDelete.test.ts @@ -0,0 +1,78 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateAMMDelete } from '../../src/models/transactions/AMMDelete' + +/** + * AMMDelete Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMDelete', function () { + let ammDelete + + beforeEach(function () { + ammDelete = { + TransactionType: 'AMMDelete', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Asset: { + currency: 'XRP', + }, + Asset2: { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + }, + Sequence: 1337, + Flags: 0, + } as any + }) + + it(`verifies valid AMMDelete`, function () { + assert.doesNotThrow(() => validateAMMDelete(ammDelete)) + assert.doesNotThrow(() => validate(ammDelete)) + }) + + it(`throws w/ missing field Asset`, function () { + delete ammDelete.Asset + const errorMessage = 'AMMDelete: missing field Asset' + assert.throws( + () => validateAMMDelete(ammDelete), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammDelete), ValidationError, errorMessage) + }) + + it(`throws w/ Asset must be a Currency`, function () { + ammDelete.Asset = 1234 + const errorMessage = 'AMMDelete: Asset must be a Currency' + assert.throws( + () => validateAMMDelete(ammDelete), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammDelete), ValidationError, errorMessage) + }) + + it(`throws w/ missing field Asset2`, function () { + delete ammDelete.Asset2 + const errorMessage = 'AMMDelete: missing field Asset2' + assert.throws( + () => validateAMMDelete(ammDelete), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammDelete), ValidationError, errorMessage) + }) + + it(`throws w/ Asset2 must be a Currency`, function () { + ammDelete.Asset2 = 1234 + const errorMessage = 'AMMDelete: Asset2 must be a Currency' + assert.throws( + () => validateAMMDelete(ammDelete), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(ammDelete), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/AMMDeposit.test.ts b/packages/xrpl/test/models/AMMDeposit.test.ts new file mode 100644 index 0000000000..0f041d86f4 --- /dev/null +++ b/packages/xrpl/test/models/AMMDeposit.test.ts @@ -0,0 +1,204 @@ +/* eslint-disable no-bitwise -- bitwise necessary for enabling flags */ +import { assert } from 'chai' + +import { AMMDepositFlags, validate, ValidationError } from '../../src' +import { validateAMMDeposit } from '../../src/models/transactions/AMMDeposit' + +/** + * AMMDeposit Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMDeposit', function () { + const LPTokenOut = { + currency: 'B3813FCAB4EE68B3D0D735D6849465A9113EE048', + issuer: 'rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg', + value: '1000', + } + let deposit + + beforeEach(function () { + deposit = { + TransactionType: 'AMMDeposit', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Asset: { + currency: 'XRP', + }, + Asset2: { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + }, + Sequence: 1337, + Flags: 0, + } as any + }) + + it(`verifies valid AMMDeposit with LPTokenOut`, function () { + deposit.LPTokenOut = LPTokenOut + deposit.Flags |= AMMDepositFlags.tfLPToken + assert.doesNotThrow(() => validateAMMDeposit(deposit)) + assert.doesNotThrow(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount`, function () { + deposit.Amount = '1000' + deposit.Flags |= AMMDepositFlags.tfSingleAsset + assert.doesNotThrow(() => validateAMMDeposit(deposit)) + assert.doesNotThrow(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount and Amount2`, function () { + deposit.Amount = '1000' + deposit.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + deposit.Flags |= AMMDepositFlags.tfTwoAsset + assert.doesNotThrow(() => validateAMMDeposit(deposit)) + assert.doesNotThrow(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount and LPTokenOut`, function () { + deposit.Amount = '1000' + deposit.LPTokenOut = LPTokenOut + deposit.Flags |= AMMDepositFlags.tfOneAssetLPToken + assert.doesNotThrow(() => validateAMMDeposit(deposit)) + assert.doesNotThrow(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount and EPrice`, function () { + deposit.Amount = '1000' + deposit.EPrice = '25' + deposit.Flags |= AMMDepositFlags.tfLimitLPToken + assert.doesNotThrow(() => validateAMMDeposit(deposit)) + assert.doesNotThrow(() => validate(deposit)) + }) + + it(`throws w/ missing field Asset`, function () { + delete deposit.Asset + const errorMessage = 'AMMDeposit: missing field Asset' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ Asset must be a Currency`, function () { + deposit.Asset = 1234 + const errorMessage = 'AMMDeposit: Asset must be a Currency' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ missing field Asset2`, function () { + delete deposit.Asset2 + const errorMessage = 'AMMDeposit: missing field Asset2' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ Asset2 must be a Currency`, function () { + deposit.Asset2 = 1234 + const errorMessage = 'AMMDeposit: Asset2 must be a Currency' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ must set at least LPTokenOut or Amount`, function () { + const errorMessage = 'AMMDeposit: must set at least LPTokenOut or Amount' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ must set Amount with Amount2`, function () { + deposit.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + const errorMessage = 'AMMDeposit: must set Amount with Amount2' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ must set Amount with EPrice`, function () { + deposit.EPrice = '25' + const errorMessage = 'AMMDeposit: must set Amount with EPrice' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ LPTokenOut must be an IssuedCurrencyAmount`, function () { + deposit.LPTokenOut = 1234 + const errorMessage = + 'AMMDeposit: LPTokenOut must be an IssuedCurrencyAmount' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ Amount must be an Amount`, function () { + deposit.Amount = 1234 + const errorMessage = 'AMMDeposit: Amount must be an Amount' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + deposit.Amount = '1000' + deposit.Amount2 = 1234 + const errorMessage = 'AMMDeposit: Amount2 must be an Amount' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) + + it(`throws w/ EPrice must be an Amount`, function () { + deposit.Amount = '1000' + deposit.EPrice = 1234 + const errorMessage = 'AMMDeposit: EPrice must be an Amount' + assert.throws( + () => validateAMMDeposit(deposit), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(deposit), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/AMMVote.test.ts b/packages/xrpl/test/models/AMMVote.test.ts new file mode 100644 index 0000000000..25f6cdeffe --- /dev/null +++ b/packages/xrpl/test/models/AMMVote.test.ts @@ -0,0 +1,90 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateAMMVote } from '../../src/models/transactions/AMMVote' + +/** + * AMMVote Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMVote', function () { + let vote + + beforeEach(function () { + vote = { + TransactionType: 'AMMVote', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Asset: { + currency: 'XRP', + }, + Asset2: { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + }, + TradingFee: 25, + Sequence: 1337, + } as any + }) + + it(`verifies valid AMMVote`, function () { + assert.doesNotThrow(() => validateAMMVote(vote)) + assert.doesNotThrow(() => validate(vote)) + }) + + it(`throws w/ missing field Asset`, function () { + delete vote.Asset + const errorMessage = 'AMMVote: missing field Asset' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws w/ Asset must be a Currency`, function () { + vote.Asset = 1234 + const errorMessage = 'AMMVote: Asset must be a Currency' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws w/ missing field Asset2`, function () { + delete vote.Asset2 + const errorMessage = 'AMMVote: missing field Asset2' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws w/ Asset2 must be a Currency`, function () { + vote.Asset2 = 1234 + const errorMessage = 'AMMVote: Asset2 must be a Currency' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws w/ missing field TradingFee`, function () { + delete vote.TradingFee + const errorMessage = 'AMMVote: missing field TradingFee' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws w/ TradingFee must be a number`, function () { + vote.TradingFee = '25' + const errorMessage = 'AMMVote: TradingFee must be a number' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws when TradingFee is greater than AMM_MAX_TRADING_FEE`, function () { + vote.TradingFee = 1001 + const errorMessage = 'AMMVote: TradingFee must be between 0 and 1000' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) + + it(`throws when TradingFee is a negative number`, function () { + vote.TradingFee = -1 + const errorMessage = 'AMMVote: TradingFee must be between 0 and 1000' + assert.throws(() => validateAMMVote(vote), ValidationError, errorMessage) + assert.throws(() => validate(vote), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/AMMWithdraw.test.ts b/packages/xrpl/test/models/AMMWithdraw.test.ts new file mode 100644 index 0000000000..47092ef724 --- /dev/null +++ b/packages/xrpl/test/models/AMMWithdraw.test.ts @@ -0,0 +1,207 @@ +/* eslint-disable no-bitwise -- bitwise necessary for enabling flags */ +import { assert } from 'chai' + +import { AMMWithdrawFlags, validate, ValidationError } from '../../src' +import { validateAMMWithdraw } from '../../src/models/transactions/AMMWithdraw' + +/** + * AMMWithdraw Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('AMMWithdraw', function () { + const LPTokenIn = { + currency: 'B3813FCAB4EE68B3D0D735D6849465A9113EE048', + issuer: 'rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg', + value: '1000', + } + let withdraw + + beforeEach(function () { + withdraw = { + TransactionType: 'AMMWithdraw', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Asset: { + currency: 'XRP', + }, + Asset2: { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + }, + Sequence: 1337, + Flags: 0, + } as any + }) + + it(`verifies valid AMMWithdraw with LPTokenIn`, function () { + withdraw.LPTokenIn = LPTokenIn + withdraw.Flags |= AMMWithdrawFlags.tfLPToken + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount`, function () { + withdraw.Amount = '1000' + withdraw.Flags |= AMMWithdrawFlags.tfSingleAsset + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount and Amount2`, function () { + withdraw.Amount = '1000' + withdraw.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + withdraw.Flags |= AMMWithdrawFlags.tfTwoAsset + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount and LPTokenIn`, function () { + withdraw.Amount = '1000' + withdraw.LPTokenIn = LPTokenIn + withdraw.Flags |= AMMWithdrawFlags.tfOneAssetLPToken + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount and EPrice`, function () { + withdraw.Amount = '1000' + withdraw.EPrice = '25' + withdraw.Flags |= AMMWithdrawFlags.tfLimitLPToken + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw one asset withdraw all`, function () { + withdraw.Amount = '1000' + withdraw.Flags |= AMMWithdrawFlags.tfOneAssetWithdrawAll + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw withdraw all`, function () { + withdraw.Flags |= AMMWithdrawFlags.tfWithdrawAll + assert.doesNotThrow(() => validateAMMWithdraw(withdraw)) + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`throws w/ missing field Asset`, function () { + delete withdraw.Asset + const errorMessage = 'AMMWithdraw: missing field Asset' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ Asset must be a Currency`, function () { + withdraw.Asset = 1234 + const errorMessage = 'AMMWithdraw: Asset must be a Currency' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ missing field Asset2`, function () { + delete withdraw.Asset2 + const errorMessage = 'AMMWithdraw: missing field Asset2' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ Asset2 must be a Currency`, function () { + withdraw.Asset2 = 1234 + const errorMessage = 'AMMWithdraw: Asset2 must be a Currency' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ must set Amount with Amount2`, function () { + withdraw.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + const errorMessage = 'AMMWithdraw: must set Amount with Amount2' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ must set Amount with EPrice`, function () { + withdraw.EPrice = '25' + const errorMessage = 'AMMWithdraw: must set Amount with EPrice' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ LPTokenIn must be an IssuedCurrencyAmount`, function () { + withdraw.LPTokenIn = 1234 + const errorMessage = + 'AMMWithdraw: LPTokenIn must be an IssuedCurrencyAmount' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ Amount must be an Amount`, function () { + withdraw.Amount = 1234 + const errorMessage = 'AMMWithdraw: Amount must be an Amount' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + withdraw.Amount = '1000' + withdraw.Amount2 = 1234 + const errorMessage = 'AMMWithdraw: Amount2 must be an Amount' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) + + it(`throws w/ EPrice must be an Amount`, function () { + withdraw.Amount = '1000' + withdraw.EPrice = 1234 + const errorMessage = 'AMMWithdraw: EPrice must be an Amount' + assert.throws( + () => validateAMMWithdraw(withdraw), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(withdraw), ValidationError, errorMessage) + }) +})