diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index a59de992..c8b02ae3 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,16 @@ "type": "UInt16" } ], + [ + "TradingFee", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], [ "Version", { @@ -771,6 +783,26 @@ "type": "UInt32" } ], + [ + "VoteWeight", + { + "nth": 47, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "DiscountedFee", + { + "nth": 48, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1121,6 +1153,16 @@ "type": "Hash256" } ], + [ + "AMMID", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "BookDirectory", { @@ -1391,6 +1433,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 +1503,56 @@ "type": "Amount" } ], + [ + "LPTokenOut", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenIn", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "EPrice", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "Price", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenBalance", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "PublicKey", { @@ -1761,6 +1883,16 @@ "type": "AccountID" } ], + [ + "AMMAccount", + { + "nth": 11, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "HookAccount", { @@ -1821,6 +1953,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 +2183,36 @@ "type": "STObject" } ], + [ + "VoteEntry", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuctionSlot", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuthAccount", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2121,6 +2303,16 @@ "type": "STArray" } ], + [ + "VoteSlots", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "Majorities", { @@ -2170,6 +2362,16 @@ "isSigningField": true, "type": "STArray" } + ], + [ + "AuthAccounts", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } ] ], "TRANSACTION_RESULTS": { @@ -2228,6 +2430,7 @@ "temUNKNOWN": -264, "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, + "temAMM_BAD_TOKENS": -261, "tefFAILURE": -199, "tefALREADY": -198, @@ -2263,6 +2466,7 @@ "terNO_RIPPLE": -90, "terQUEUED": -89, "terPRE_TICKET": -88, + "terNO_AMM": -87, "tesSUCCESS": 0, @@ -2311,7 +2515,14 @@ "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, "tecINSUFFICIENT_FUNDS": 159, "tecOBJECT_NOT_FOUND": 160, - "tecINSUFFICIENT_PAYMENT": 161 + "tecINSUFFICIENT_PAYMENT": 161, + "tecAMM_UNFUNDED": 162, + "tecAMM_BALANCE": 163, + "tecAMM_FAILED_DEPOSIT": 164, + "tecAMM_FAILED_WITHDRAW": 165, + "tecAMM_INVALID_TOKENS": 166, + "tecAMM_FAILED_BID": 167, + "tecAMM_FAILED_VOTE": 168 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2343,6 +2554,11 @@ "NFTokenCreateOffer": 27, "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, + "AMMCreate": 35, + "AMMDeposit": 36, + "AMMWithdraw": 37, + "AMMVote": 38, + "AMMBid": 39, "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 fd8b02c2..26b3849a 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 00000000..3c3925b9 --- /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/fixtures/codec-fixtures.json b/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json index 15aa20f1..5758dbf5 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": "12002315000A220000000024000000026140000000000027106840000000000000016BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440913E39EC2BA0E5BC4C5DF1222B1AE9E76758F2B8FFEF1F056076147BB0ADC8117CD0296360DA08B3D48BE9EFC8693C03A253E0D9F166C19CA8D936F9E61A1100811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMCreate", + "TxnSignature": "913E39EC2BA0E5BC4C5DF1222B1AE9E76758F2B8FFEF1F056076147BB0ADC8117CD0296360DA08B3D48BE9EFC8693C03A253E0D9F166C19CA8D936F9E61A1100", + "Amount": "10000", + "Amount2": { + "currency": "ETH", + "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", + "value": "10000" + }, + "TradingFee": 10, + "Fee": "1", + "Flags": 0, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0" + } + }, + { + "binary": "120024220001000024000000026840000000000000016014D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D074409EEE8CF88C668B955E7EEAB1B4A1B059EDF4F51B7F1546810F87E3E48B09237F015C651E37FB40A979E00EA21361D4E18D7A33DB7DD23070CEEAB2648AB3BB0D811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMDeposit", + "TxnSignature": "9EEE8CF88C668B955E7EEAB1B4A1B059EDF4F51B7F1546810F87E3E48B09237F015C651E37FB40A979E00EA21361D4E18D7A33DB7DD23070CEEAB2648AB3BB0D", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "1", + "Flags": 65536, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0" + } + }, + { + "binary": "120024220008000024000000026140000000000003E86840000000000000017321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440BD18A6E2B10B451F61CFADC32B59A0243702DC5DAAE556D51CB9C79981D40C78101FFA9DE6163CFBDF6E7578DF02F2AE3B8A5AB60697E0746D65064D91E8F90A811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "1", + "Flags": 524288, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "BD18A6E2B10B451F61CFADC32B59A0243702DC5DAAE556D51CB9C79981D40C78101FFA9DE6163CFBDF6E7578DF02F2AE3B8A5AB60697E0746D65064D91E8F90A" + } + }, + { + "binary": "120024220010000024000000026140000000000003E86840000000000000016BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440E0B1AE32A0F731BF0CEF0D019295BD7F35B22F11A5962F65FA99EE4D38993B14B53DB11C15E36D756E282812E9015D38A6F225940A157693F43F9B795C59950F811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "1", + "Flags": 1048576, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "E0B1AE32A0F731BF0CEF0D019295BD7F35B22F11A5962F65FA99EE4D38993B14B53DB11C15E36D756E282812E9015D38A6F225940A157693F43F9B795C59950F" + } + }, + { + "binary": "120024220020000024000000026140000000000003E86840000000000000016014D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440452BC59F9EE12C224EC983EFDF580F20C4A50E897105FD1FB13520D9753CFB02BD210599181574DF6AD0DB6A42C1EA48D9E48FC3D11B9008E4C76FBB163D5B00811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "1", + "Flags": 2097152, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "452BC59F9EE12C224EC983EFDF580F20C4A50E897105FD1FB13520D9753CFB02BD210599181574DF6AD0DB6A42C1EA48D9E48FC3D11B9008E4C76FBB163D5B00" + } + }, + { + "binary": "120024220040000024000000026140000000000003E8684000000000000001601640000000000000197321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440DD6685DC586FAA6AD2D50D785900122EB147D4AC09A55D7080267A9B38180F87CEC44B823359FC3F0AC0104D47B53FFC6B80415664C3C4582672420A0100F70C811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "1", + "Flags": 4194304, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "DD6685DC586FAA6AD2D50D785900122EB147D4AC09A55D7080267A9B38180F87CEC44B823359FC3F0AC0104D47B53FFC6B80415664C3C4582672420A0100F70C" + } + }, + { + "binary": "120025220001000024000000026840000000000000016015D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0744066944797E9F03808C9A00AAEFF786AD74FEB2E64B51A9601E89ABA820AAA15927C2E961A9CCA22C4B0D2A2B55E342BD6E297BD765B6F4D3FDCA578A3416BB505811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "1", + "Flags": 65536, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "66944797E9F03808C9A00AAEFF786AD74FEB2E64B51A9601E89ABA820AAA15927C2E961A9CCA22C4B0D2A2B55E342BD6E297BD765B6F4D3FDCA578A3416BB505" + } + }, + { + "binary": "120025220008000024000000026140000000000003E86840000000000000017321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440E30397CE7E99B13D35FFB5C66725B17F4F103675E10293C7B1D63C1BE3FA81B884BD3FBD31B52F6B811F99C5FBB5102D170EC379C268DF80DABF04E7F2DD4F0C811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "1", + "Flags": 524288, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "E30397CE7E99B13D35FFB5C66725B17F4F103675E10293C7B1D63C1BE3FA81B884BD3FBD31B52F6B811F99C5FBB5102D170EC379C268DF80DABF04E7F2DD4F0C" + } + }, + { + "binary": "120025220010000024000000026140000000000003E86840000000000000016BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D07440C0818312B269A4EF16C1C7EBBB74EFD1852A288BB214A714B8BE3B5F4B2F9CFDFF4F66C931B8434244A8016035B9EC9493B7CF5E0ACF4570A88DF808D79E4300811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "1", + "Flags": 1048576, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "C0818312B269A4EF16C1C7EBBB74EFD1852A288BB214A714B8BE3B5F4B2F9CFDFF4F66C931B8434244A8016035B9EC9493B7CF5E0ACF4570A88DF808D79E4300" + } + }, + { + "binary": "120025220020000024000000026140000000000003E86840000000000000016015D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0744073552B3DC7AE99DDF4E4FF0D60E6D0BE4688E3474D363603FA25DA6AD8BBA8F0E4E3EA82ADB2B57F5B9A6C379969E00095546DDA0E74FF3D0F0689351C2F8C06811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "1", + "Flags": 2097152, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "73552B3DC7AE99DDF4E4FF0D60E6D0BE4688E3474D363603FA25DA6AD8BBA8F0E4E3EA82ADB2B57F5B9A6C379969E00095546DDA0E74FF3D0F0689351C2F8C06" + } + }, + { + "binary": "120025220040000024000000026140000000000003E8684000000000000001601640000000000000197321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0744023BAFE5BFE58E7BF0B02B5875983D007C10796C8E62A190BF688EBE5D8A104DAD2DE7EDE995FE2E494883FD8140F38E22E3376A2F49C50EFCAA00C7499A4690E811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "1", + "Flags": 4194304, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "23BAFE5BFE58E7BF0B02B5875983D007C10796C8E62A190BF688EBE5D8A104DAD2DE7EDE995FE2E494883FD8140F38E22E3376A2F49C50EFCAA00C7499A4690E" + } + }, + { + "binary": "120027220000000024000000026840000000000000016CD4C8E1BC9BF04000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0486DD4CC6F3B40B6C000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D074406B2A1548E6DC14681356C27CCBE7072CAB2AD8C72D0D7A045916FB0E0DBE6BF71A429CC519E9200172829D3EEF79100899D3A8710C1C3C1A2B664FD64086AD0A811462D4D845D20B4F09CFEA8BB4C01063D99FC9673EF01AE01C81149A91957F8F16BC57F3F200CD8C98375BF1791586E1F10318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "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": "1", + "Flags": 0, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "6B2A1548E6DC14681356C27CCBE7072CAB2AD8C72D0D7A045916FB0E0DBE6BF71A429CC519E9200172829D3EEF79100899D3A8710C1C3C1A2B664FD64086AD0A" + } + }, + { + "binary": "1200261500EA220000000024000000026840000000000000017321ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0744072767CF9A0F5E9C9DA6BBB6E84905B0ECDF122D3E2D730843EFD377521E8E73664AD809D0A54E8C75CD1735ACB64E310BB49FDED10913FA150B8C006D4ACEC00811462D4D845D20B4F09CFEA8BB4C01063D99FC9673E0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rwr2UWxNwoBdysPSiDDraTQjAQKZEeZAcV", + "TransactionType": "AMMVote", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "TradingFee": 234, + "Fee": "1", + "Flags": 0, + "Sequence": 2, + "SigningPubKey": "ED8A00C1D29E762266576408B08D583B987673550655F930635678B436D5CDF7D0", + "TxnSignature": "72767CF9A0F5E9C9DA6BBB6E84905B0ECDF122D3E2D730843EFD377521E8E73664AD809D0A54E8C75CD1735ACB64E310BB49FDED10913FA150B8C006D4ACEC00" + } }], "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 c6537557..d3470a3b 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -59,6 +59,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 6543b9d3..33cfc860 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -25,6 +25,9 @@ import { AccountOffersResponse, AccountTxRequest, AccountTxResponse, + // AMM methods + AMMInfoRequest, + AMMInfoResponse, GatewayBalancesRequest, GatewayBalancesResponse, NoRippleCheckRequest, @@ -308,6 +311,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 2eeb7b17..e8b3b76f 100644 --- a/packages/xrpl/src/models/common/index.ts +++ b/packages/xrpl/src/models/common/index.ts @@ -4,7 +4,7 @@ interface XRP { currency: 'XRP' } -interface IssuedCurrency { +export interface IssuedCurrency { currency: string issuer: string } diff --git a/packages/xrpl/src/models/methods/ammInfo.ts b/packages/xrpl/src/models/methods/ammInfo.ts new file mode 100644 index 00000000..75c644ed --- /dev/null +++ b/packages/xrpl/src/models/methods/ammInfo.ts @@ -0,0 +1,147 @@ +import { Amount, Currency, IssuedCurrencyAmount } from '../common' + +import { BaseRequest, BaseResponse } from './baseMethod' + +/** + * The `amm_info` command retrieves information about an AMM instance. + * Returns an {@link AMMInfoResponse}. + * + * @category Requests + */ +export interface AMMInfoRequest extends BaseRequest { + command: 'amm_info' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + * Both asset and asset2 must be defined to specify an AMM instance. + */ + asset: Currency + + /** + * Specifies the other pool asset of the AMM instance. + * Both asset and asset2 must be defined to specify an AMM instance. + */ + asset2: Currency +} + +interface AuthAccount { + account: string +} + +interface VoteEntry { + account: string + trading_fee: number + vote_weight: number +} + +/** + * Response expected from an {@link AMMInfoRequest}. + * + * @category Responses + */ +export interface AMMInfoResponse extends BaseResponse { + result: { + amm: { + /** + * The account that tracks the balance of LPTokens between the AMM instance via Trustline. + */ + amm_account: string + + /** + * One of the pool assets (XRP or token) of the AMM instance. + */ + amount: Amount + + /** + * The other pool asset of the AMM instance. + */ + 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 + + /** + * Details of the current owner of the auction slot. + */ + auction_slot?: { + /** + * 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. + */ + auth_accounts: 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. + */ + discounted_fee: number + + /** + * The time when this slot expires, in seconds since the Ripple Epoch. + */ + expiration: string + + /** + * The amount the auction owner paid to win this slot, in LPTokens. + */ + price: Amount + + /** + * Total slot time of 24-hours is divided into 20 equal time intervals. + */ + time_interval: number + } + + /** + * 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. + */ + lp_token: IssuedCurrencyAmount + + /** + * Specifies the fee, in basis point, to be charged to the traders for the trades + * executed against the AMM instance. Trading fee is a percentage of the trading volume. + * Valid values for this field are between 0 and 1000 inclusive. + * A value of 1 is equivalent to 1/10 bps or 0.001%, allowing trading fee + * between 0% and 1%. This field is required. + */ + trading_fee: number + + /** + * Keeps a track of up to eight active votes for the instance. + */ + vote_slots?: VoteEntry[] + } + + /** + * 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 7c5aee39..7f40a5d8 100644 --- a/packages/xrpl/src/models/methods/index.ts +++ b/packages/xrpl/src/models/methods/index.ts @@ -16,6 +16,7 @@ import { AccountOffersResponse, } from './accountOffers' import { AccountTxRequest, AccountTxResponse } from './accountTx' +import { AMMInfoRequest, AMMInfoResponse } from './ammInfo' import { ErrorResponse } from './baseMethod' import { BookOffersRequest, BookOffer, BookOffersResponse } from './bookOffers' import { ChannelVerifyRequest, ChannelVerifyResponse } from './channelVerify' @@ -87,6 +88,7 @@ type Request = | AccountObjectsRequest | AccountOffersRequest | AccountTxRequest + | AMMInfoRequest | GatewayBalancesRequest | NoRippleCheckRequest // ledger methods @@ -137,6 +139,7 @@ type Response = | AccountObjectsResponse | AccountOffersResponse | AccountTxResponse + | AMMInfoResponse | GatewayBalancesResponse | NoRippleCheckResponse // ledger methods @@ -195,6 +198,8 @@ export { AccountOffersResponse, AccountTxRequest, AccountTxResponse, + AMMInfoRequest, + AMMInfoResponse, GatewayBalancesRequest, GatewayBalancesResponse, NoRippleCheckRequest, diff --git a/packages/xrpl/src/models/transactions/AMMBid.ts b/packages/xrpl/src/models/transactions/AMMBid.ts new file mode 100644 index 00000000..eabbf7eb --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMBid.ts @@ -0,0 +1,106 @@ +/* eslint-disable complexity -- required for validateAMMBid */ +import { ValidationError } from '../../errors' +import { Amount, Currency } from '../common' + +import { + BaseTransaction, + isAmount, + isCurrency, + validateBaseTransaction, +} from './common' + +const MAX_AUTH_ACCOUNTS = 4 + +interface AuthAccount { + AuthAccount: { + Account: string + } +} + +/** + * AMMBid is used for submitting a vote for the trading fee of an AMM Instance. + * + * Any XRPL account that holds LPToken for an AMM instance may submit this + * transaction to vote for the trading fee for that instance. + */ +export interface AMMBid extends BaseTransaction { + TransactionType: 'AMMBid' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + */ + Asset: Currency + + /** + * Specifies the other pool asset of the AMM instance. + */ + Asset2: Currency + + /** + * This field represents the minimum price that the bidder wants to pay for the slot. + * It is specified in units of LPToken. If specified let BidMin be X and let + * the slot-price computed by price scheduling algorithm be Y, then bidder always pays + * the max(X, Y). + */ + BidMin?: Amount + + /** + * This field represents the maximum price that the bidder wants to pay for the slot. + * It is specified in units of LPToken. + */ + BidMax?: Amount + + /** + * This field represents an array of XRPL account IDs that are authorized to trade + * at the discounted fee against the AMM instance. + * A maximum of four accounts can be provided. + */ + 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 an Issue') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMBid: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMBid: Asset2 must be an Issue') + } + + 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}`, + ) + } + } +} diff --git a/packages/xrpl/src/models/transactions/AMMCreate.ts b/packages/xrpl/src/models/transactions/AMMCreate.ts new file mode 100644 index 00000000..9ce8b68d --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMCreate.ts @@ -0,0 +1,76 @@ +import { ValidationError } from '../../errors' +import { Amount } from '../common' + +import { BaseTransaction, isAmount, validateBaseTransaction } from './common' + +export const AMM_MAX_TRADING_FEE = 1000 + +/** + * AMMCreate is used to create AccountRoot and the corresponding + * AMM ledger entries. + * + * This allows for the creation of only one AMM instance per unique asset pair. + */ +export interface AMMCreate extends BaseTransaction { + TransactionType: 'AMMCreate' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + */ + Amount: Amount + + /** + * Specifies the other pool asset of the AMM instance. + */ + Amount2: Amount + + /** + * Specifies the fee, in basis point, to be charged + * to the traders for the trades executed against the AMM instance. + * Trading fee is a percentage of the trading volume. + * Valid values for this field are between 0 and 1000 inclusive. + * A value of 1 is equivalent to 1/10 bps or 0.001%, allowing trading fee + * between 0% and 1%. + */ + 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/AMMDeposit.ts b/packages/xrpl/src/models/transactions/AMMDeposit.ts new file mode 100644 index 00000000..856ef2e7 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMDeposit.ts @@ -0,0 +1,135 @@ +/* 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 +} + +/** + * AMMDeposit is the deposit transaction used to add liquidity to the AMM instance pool, + * thus obtaining some share of the instance's pools in the form of LPTokenOut. + * + * The following are the recommended valid combinations: + * - LPTokenOut + * - Amount + * - Amount and Amount2 + * - Amount and LPTokenOut + * - Amount and EPrice + */ +export interface AMMDeposit extends BaseTransaction { + TransactionType: 'AMMDeposit' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + */ + Asset: Currency + + /** + * Specifies the other pool asset of the AMM instance. + */ + Asset2: Currency + + /** + * Specifies the amount of shares of the AMM instance pools that the trader + * wants to redeem or trade in. + */ + LPTokenOut?: IssuedCurrencyAmount + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance to + * deposit more of its value. + */ + Amount?: Amount + + /** + * Specifies the other pool asset of the AMM instance to deposit more of + * its value. + */ + Amount2?: Amount + + /** + * Specifies the maximum effective-price that LPTokenOut can be traded out. + */ + EPrice?: Amount +} + +/** + * 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 an Issue') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMDeposit: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMDeposit: Asset2 must be an Issue') + } + + 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 00000000..98734823 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMVote.ts @@ -0,0 +1,73 @@ +import { ValidationError } from '../../errors' +import { Currency } from '../common' + +import { AMM_MAX_TRADING_FEE } from './AMMCreate' +import { BaseTransaction, isCurrency, validateBaseTransaction } from './common' + +/** + * AMMVote is used for submitting a vote for the trading fee of an AMM Instance. + * + * Any XRPL account that holds LPToken for an AMM instance may submit this + * transaction to vote for the trading fee for that instance. + */ +export interface AMMVote extends BaseTransaction { + TransactionType: 'AMMVote' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + */ + Asset: Currency + + /** + * Specifies the other pool asset of the AMM instance. + */ + Asset2: Currency + + /** + * Specifies the fee, in basis point. + * Valid values for this field are between 0 and 1000 inclusive. + * A value of 1 is equivalent to 1/10 bps or 0.001%, allowing trading fee + * between 0% and 1%. This field is required. + */ + 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 an Issue') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMVote: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMVote: Asset2 must be an Issue') + } + + 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 00000000..36bcbf42 --- /dev/null +++ b/packages/xrpl/src/models/transactions/AMMWithdraw.ts @@ -0,0 +1,137 @@ +/* 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 +} + +/** + * AMMWithdraw is the withdraw transaction used to remove liquidity from the AMM + * instance pool, thus redeeming some share of the pools that one owns in the form + * of LPTokenIn. + * + * The following are the recommended valid combinations: + * - LPTokenIn + * - Amount + * - Amount and Amount2 + * - Amount and LPTokenIn + * - Amount and EPrice + */ +export interface AMMWithdraw extends BaseTransaction { + TransactionType: 'AMMWithdraw' + + /** + * Specifies one of the pool assets (XRP or token) of the AMM instance. + */ + Asset: Currency + + /** + * Specifies the other pool asset of the AMM instance. + */ + Asset2: Currency + + /** + * Specifies the amount of shares of the AMM instance pools that the trader + * wants to redeem or trade in. + */ + LPTokenIn?: IssuedCurrencyAmount + + /** + * Specifies one of the pools assets that the trader wants to remove. + * If the asset is XRP, then the Amount is a string specifying the number of drops. + * Otherwise it is an IssuedCurrencyAmount object. + */ + Amount?: Amount + + /** + * Specifies the other pool asset that the trader wants to remove. + */ + Amount2?: Amount + + /** + * Specifies the effective-price of the token out after successful execution of + * the transaction. + */ + EPrice?: Amount +} + +/** + * 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 an Issue') + } + + if (tx.Asset2 == null) { + throw new ValidationError('AMMWithdraw: missing field Asset2') + } + + if (!isCurrency(tx.Asset2)) { + throw new ValidationError('AMMWithdraw: Asset2 must be an Issue') + } + + 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 8bceabe6..d2383d73 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 49944424..354b50ad 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -7,6 +7,19 @@ export { AccountSet, } from './accountSet' export { AccountDelete } from './accountDelete' +export { AMMBid } from './AMMBid' +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 dfe0ffbb..a67326ad 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -6,6 +6,11 @@ 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 { 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' @@ -54,6 +59,11 @@ import { TrustSet, validateTrustSet } from './trustSet' export type Transaction = | AccountDelete | AccountSet + | AMMBid + | AMMDeposit + | AMMCreate + | AMMVote + | AMMWithdraw | CheckCancel | CheckCash | CheckCreate @@ -112,6 +122,26 @@ export function validate(transaction: Record): void { validateAccountSet(tx) break + case 'AMMBid': + validateAMMBid(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 f185c99b..ad5dcaca 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 '.' @@ -61,55 +54,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 c7a0d75f..1e474e35 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -143,7 +143,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 @@ -175,9 +175,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 00000000..0ef28dce --- /dev/null +++ b/packages/xrpl/test/models/AMMBid.test.ts @@ -0,0 +1,123 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' + +/** + * 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(() => validate(bid)) + }) + + it(`throws w/ missing field Asset`, function () { + delete bid.Asset + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: missing field Asset', + ) + }) + + it(`throws w/ Asset must be an Issue`, function () { + bid.Asset = 1234 + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: Asset must be an Issue', + ) + }) + + it(`throws w/ missing field Asset2`, function () { + delete bid.Asset2 + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: missing field Asset2', + ) + }) + + it(`throws w/ Asset2 must be an Issue`, function () { + bid.Asset2 = 1234 + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: Asset2 must be an Issue', + ) + }) + + it(`throws w/ BidMin must be an Amount`, function () { + bid.BidMin = 5 + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: BidMin must be an Amount', + ) + }) + + it(`throws w/ BidMax must be an Amount`, function () { + bid.BidMax = 10 + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: BidMax must be an Amount', + ) + }) + + it(`throws w/ AuthAccounts length must not be greater than 4`, function () { + bid.AuthAccounts.push({ + AuthAccount: { + Account: 'r3X6noRsvaLapAKCG78zAtWcbhB3sggS1s', + }, + }) + + assert.throws( + () => validate(bid), + ValidationError, + 'AMMBid: AuthAccounts length must not be greater than 4', + ) + }) +}) diff --git a/packages/xrpl/test/models/AMMCreate.test.ts b/packages/xrpl/test/models/AMMCreate.test.ts new file mode 100644 index 00000000..03b5028b --- /dev/null +++ b/packages/xrpl/test/models/AMMCreate.test.ts @@ -0,0 +1,103 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' + +/** + * 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(() => validate(ammCreate)) + }) + + it(`throws w/ missing Amount`, function () { + delete ammCreate.Amount + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: missing field Amount', + ) + }) + + it(`throws w/ Amount must be an Amount`, function () { + ammCreate.Amount = 1000 + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: Amount must be an Amount', + ) + }) + + it(`throws w/ missing Amount2`, function () { + delete ammCreate.Amount2 + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: missing field Amount2', + ) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + ammCreate.Amount2 = 1000 + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: Amount2 must be an Amount', + ) + }) + + it(`throws w/ missing TradingFee`, function () { + delete ammCreate.TradingFee + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: missing field TradingFee', + ) + }) + + it(`throws w/ TradingFee must be a number`, function () { + ammCreate.TradingFee = '12' + assert.throws( + () => validate(ammCreate), + ValidationError, + 'AMMCreate: TradingFee must be a number', + ) + }) + + it(`throws when TradingFee is greater than 1000`, function () { + ammCreate.TradingFee = 1001 + assert.throws( + () => validate(ammCreate), + ValidationError, + `AMMCreate: TradingFee must be between 0 and 1000`, + ) + }) + + it(`throws when TradingFee is a negative number`, function () { + ammCreate.TradingFee = -1 + assert.throws( + () => validate(ammCreate), + ValidationError, + `AMMCreate: TradingFee must be between 0 and 1000`, + ) + }) +}) diff --git a/packages/xrpl/test/models/AMMDeposit.test.ts b/packages/xrpl/test/models/AMMDeposit.test.ts new file mode 100644 index 00000000..9eeb6ec3 --- /dev/null +++ b/packages/xrpl/test/models/AMMDeposit.test.ts @@ -0,0 +1,175 @@ +/* eslint-disable no-bitwise -- bitwise necessary for enabling flags */ +import { assert } from 'chai' + +import { AMMDepositFlags, validate, ValidationError } from '../../src' + +/** + * 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(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount`, function () { + deposit.Amount = '1000' + deposit.Flags |= AMMDepositFlags.tfSingleAsset + 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(() => validate(deposit)) + }) + + it(`verifies valid AMMDeposit with Amount and LPTokenOut`, function () { + deposit.Amount = '1000' + deposit.LPTokenOut = LPTokenOut + deposit.Flags |= AMMDepositFlags.tfOneAssetLPToken + 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(() => validate(deposit)) + }) + + it(`throws w/ missing field Asset`, function () { + delete deposit.Asset + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: missing field Asset', + ) + }) + + it(`throws w/ Asset must be an Issue`, function () { + deposit.Asset = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: Asset must be an Issue', + ) + }) + + it(`throws w/ missing field Asset2`, function () { + delete deposit.Asset2 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: missing field Asset2', + ) + }) + + it(`throws w/ Asset2 must be an Issue`, function () { + deposit.Asset2 = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: Asset2 must be an Issue', + ) + }) + + it(`throws w/ must set at least LPTokenOut or Amount`, function () { + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: must set at least LPTokenOut or Amount', + ) + }) + + it(`throws w/ must set Amount with Amount2`, function () { + deposit.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: must set Amount with Amount2', + ) + }) + + it(`throws w/ must set Amount with EPrice`, function () { + deposit.EPrice = '25' + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: must set Amount with EPrice', + ) + }) + + it(`throws w/ LPTokenOut must be an IssuedCurrencyAmount`, function () { + deposit.LPTokenOut = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: LPTokenOut must be an IssuedCurrencyAmount', + ) + }) + + it(`throws w/ Amount must be an Amount`, function () { + deposit.Amount = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: Amount must be an Amount', + ) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + deposit.Amount = '1000' + deposit.Amount2 = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: Amount2 must be an Amount', + ) + }) + + it(`throws w/ EPrice must be an Amount`, function () { + deposit.Amount = '1000' + deposit.EPrice = 1234 + assert.throws( + () => validate(deposit), + ValidationError, + 'AMMDeposit: EPrice must be an Amount', + ) + }) +}) diff --git a/packages/xrpl/test/models/AMMVote.test.ts b/packages/xrpl/test/models/AMMVote.test.ts new file mode 100644 index 00000000..765690cc --- /dev/null +++ b/packages/xrpl/test/models/AMMVote.test.ts @@ -0,0 +1,104 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' + +/** + * 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(() => validate(vote)) + }) + + it(`throws w/ missing field Asset`, function () { + delete vote.Asset + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: missing field Asset', + ) + }) + + it(`throws w/ Asset must be an Issue`, function () { + vote.Asset = 1234 + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: Asset must be an Issue', + ) + }) + + it(`throws w/ missing field Asset2`, function () { + delete vote.Asset2 + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: missing field Asset2', + ) + }) + + it(`throws w/ Asset2 must be an Issue`, function () { + vote.Asset2 = 1234 + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: Asset2 must be an Issue', + ) + }) + + it(`throws w/ missing field TradingFee`, function () { + delete vote.TradingFee + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: missing field TradingFee', + ) + }) + + it(`throws w/ TradingFee must be a number`, function () { + vote.TradingFee = '25' + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: TradingFee must be a number', + ) + }) + + it(`throws when TradingFee is greater than AMM_MAX_TRADING_FEE`, function () { + vote.TradingFee = 1001 + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: TradingFee must be between 0 and 1000', + ) + }) + + it(`throws when TradingFee is a negative number`, function () { + vote.TradingFee = -1 + assert.throws( + () => validate(vote), + ValidationError, + 'AMMVote: TradingFee must be between 0 and 1000', + ) + }) +}) diff --git a/packages/xrpl/test/models/AMMWithdraw.test.ts b/packages/xrpl/test/models/AMMWithdraw.test.ts new file mode 100644 index 00000000..27a5c5c0 --- /dev/null +++ b/packages/xrpl/test/models/AMMWithdraw.test.ts @@ -0,0 +1,178 @@ +/* eslint-disable no-bitwise -- bitwise necessary for enabling flags */ +import { assert } from 'chai' + +import { AMMWithdrawFlags, validate, ValidationError } from '../../src' + +/** + * 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(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount`, function () { + withdraw.Amount = '1000' + withdraw.Flags |= AMMWithdrawFlags.tfSingleAsset + 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(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw with Amount and LPTokenIn`, function () { + withdraw.Amount = '1000' + withdraw.LPTokenIn = LPTokenIn + withdraw.Flags |= AMMWithdrawFlags.tfOneAssetLPToken + 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(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw one asset withdraw all`, function () { + withdraw.Amount = '1000' + withdraw.Flags |= AMMWithdrawFlags.tfOneAssetWithdrawAll + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`verifies valid AMMWithdraw withdraw all`, function () { + withdraw.Flags |= AMMWithdrawFlags.tfWithdrawAll + assert.doesNotThrow(() => validate(withdraw)) + }) + + it(`throws w/ missing field Asset`, function () { + delete withdraw.Asset + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: missing field Asset', + ) + }) + + it(`throws w/ Asset must be an Issue`, function () { + withdraw.Asset = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: Asset must be an Issue', + ) + }) + + it(`throws w/ missing field Asset2`, function () { + delete withdraw.Asset2 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: missing field Asset2', + ) + }) + + it(`throws w/ Asset2 must be an Issue`, function () { + withdraw.Asset2 = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: Asset2 must be an Issue', + ) + }) + + it(`throws w/ must set Amount with Amount2`, function () { + withdraw.Amount2 = { + currency: 'ETH', + issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', + value: '2.5', + } + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: must set Amount with Amount2', + ) + }) + + it(`throws w/ must set Amount with EPrice`, function () { + withdraw.EPrice = '25' + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: must set Amount with EPrice', + ) + }) + + it(`throws w/ LPTokenIn must be an IssuedCurrencyAmount`, function () { + withdraw.LPTokenIn = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: LPTokenIn must be an IssuedCurrencyAmount', + ) + }) + + it(`throws w/ Amount must be an Amount`, function () { + withdraw.Amount = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: Amount must be an Amount', + ) + }) + + it(`throws w/ Amount2 must be an Amount`, function () { + withdraw.Amount = '1000' + withdraw.Amount2 = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: Amount2 must be an Amount', + ) + }) + + it(`throws w/ EPrice must be an Amount`, function () { + withdraw.Amount = '1000' + withdraw.EPrice = 1234 + assert.throws( + () => validate(withdraw), + ValidationError, + 'AMMWithdraw: EPrice must be an Amount', + ) + }) +})