Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ExpandedSignerList amendment support #406

Merged
merged 12 commits into from
Sep 13, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [[Unreleased]]
### Added:
- Support for ExpandedSignerList amendment that expands the maximum signer list to 32 entries
- Function to parse the final account balances from a transaction's metadata
- Function to parse order book changes from a transaction's metadata
- Support for Ed25519 seeds that don't use the `sEd` prefix
Expand Down
133 changes: 133 additions & 0 deletions tests/unit/models/transactions/test_signer_list_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,136 @@ def test_signer_entries_signer_quorum_valid(self):
signer_entries=_SIGNER_ENTRIES_VALID,
)
self.assertTrue(tx.is_valid())

khancode marked this conversation as resolved.
Show resolved Hide resolved
def test_max_signer_entries_above_8_below_32(self):
signers = [
"rBFBipte4nAQCTsRxd2czwvSurhCpAf4X6",
"r3ijUH32iiy9tYNj3rD7hKWYjy1BFUxngm",
"rpwq8vi4Mn3L5kDJmb8Mg59CanPFPzMCnj",
"rB72Gzqfejai46nkA4HaKYBHwAnn2yUoT4",
"rGqsJSAW71pCfUwDD5m52bLw69RzFg6kMW",
"rs8smPRA31Ym4mGxb1wzgwxtU5eVK82Gyk",
"rLrugpGxzezUQLDh7Jv1tZpouuV4MQLbU9",
"rUQ6zLXQdh1jJLGwMXp9P8rgi42kwuafzs",
"rMjY8sPdfxsyRrnVKQcutxr4mTHNXy9dEF",
]
signer_entries = []
for acc in signers:
signer_entries.append(
SignerEntry(
account=acc,
signer_weight=1,
)
)

tx = SignerListSet(
account=_ACCOUNT,
fee=_FEE,
sequence=_SEQUENCE,
signer_quorum=9,
signer_entries=signer_entries,
)
self.assertTrue(tx.is_valid())

def test_max_signer_entries_exceeded(self):
signers = [
"rBFBipte4nAQCTsRxd2czwvSurhCpAf4X6",
"r3ijUH32iiy9tYNj3rD7hKWYjy1BFUxngm",
"rpwq8vi4Mn3L5kDJmb8Mg59CanPFPzMCnj",
"rB72Gzqfejai46nkA4HaKYBHwAnn2yUoT4",
"rGqsJSAW71pCfUwDD5m52bLw69RzFg6kMW",
"rs8smPRA31Ym4mGxb1wzgwxtU5eVK82Gyk",
"rLrugpGxzezUQLDh7Jv1tZpouuV4MQLbU9",
"rUQ6zLXQdh1jJLGwMXp9P8rgi42kwuafzs",
"rMjY8sPdfxsyRrnVKQcutxr4mTHNXy9dEF",
"rUaxYLeFGm6SmMoa2WCqLKSyHwJyvaQmeG",
"r9wUfeVtqMfqrcDTfCpNYbNZvs5q9M9Rpo",
"rQncVNak5kvJGPUFa6fuKH7t8Usjs7Np1c",
"rnwbSSnPbVbUzuBa4etkeYrfy5v7SyhtPu",
"rDXh5D3t48MdBJyXByXq47k5P8Kuf1758B",
"rh1D4jd2mAiqUPHfAZ2cY9Nbfa3kAkaQXP",
"r9T129tXgtnyfGoLeS35c2HctaZAZSQoCH",
"rUd2uKsyCWfJP7Ve36mKoJbNCA7RYThnYk",
"r326x8PaAFtnaH7uoxaKrcDWuwpeHn4wDa",
"rpN3mkXkYhfNadcXPrY4LniM1KpM3egyQM",
"rsPKbR155hz1zrA4pSJp5Y2fxasZAatcHb",
"rsyWFLaEKTpaoSJusjpcDvGexuHCwMnqss",
"rUbc5RXfyF81oLDMgd3d7jpY9YMNMZG4XN",
"rGpYHM88BZe1iVKFHm5xiWYYxR74oxJEXf",
"rPsetWAtR1KxDtxzgHjRMD7Rc87rvXk5nD",
"rwSeNhL6Hi34igr12mCr61jY42psfTkWTq",
"r46Mygy98qjkDhVB6qs4sBnqaf7FPiA2vU",
"r4s8GmeYN4CiwVate1nMUvwMQbundqf5cW",
"rKAr4dQWDYG8cG2hSwJUVp4ry4WNaWiNgp",
"rPWXRLp1vqeUHEH3WiSKuyo9GM9XhaENQU",
"rPgmdBdRKGmndxNEYxUrrsYCZaS6go9RvW",
"rPDJZ9irzgwKRKScfEmuJMvUgrqZAJNCbL",
"rDuU2uSXMfEaoxN1qW8sj7aUNFLGEn3Hr2",
"rsbjSjA4TCB9gtm7x7SrWbZHB6g4tt9CGU",
]
signer_entries = []
for acc in signers:
signer_entries.append(
SignerEntry(
account=acc,
signer_weight=1,
)
)

with self.assertRaises(XRPLModelException):
SignerListSet(
account=_ACCOUNT,
fee=_FEE,
sequence=_SEQUENCE,
signer_quorum=33,
signer_entries=signer_entries,
)

def test_signer_entries_with_wallet_locator(self):
signer_entries = [
SignerEntry(
account="rBFBipte4nAQCTsRxd2czwvSurhCpAf4X6",
signer_weight=1,
wallet_locator="CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAF"
"ECAFECAFE",
),
SignerEntry(
account="r3ijUH32iiy9tYNj3rD7hKWYjy1BFUxngm",
signer_weight=1,
),
SignerEntry(
account="rpwq8vi4Mn3L5kDJmb8Mg59CanPFPzMCnj",
signer_weight=1,
wallet_locator="0000000000000000000000000000000000000000000000000000000"
"0DEADBEEF",
),
]
tx = SignerListSet(
account=_ACCOUNT,
fee=_FEE,
sequence=_SEQUENCE,
signer_quorum=3,
signer_entries=signer_entries,
)
self.assertTrue(tx.is_valid())

def test_signer_entries_with_invalid_wallet_locator(self):
signer_entries = [
SignerEntry(
account="rBFBipte4nAQCTsRxd2czwvSurhCpAf4X6",
signer_weight=1,
wallet_locator="not_valid",
),
SignerEntry(
account="r3ijUH32iiy9tYNj3rD7hKWYjy1BFUxngm",
signer_weight=1,
),
]
with self.assertRaises(XRPLModelException):
SignerListSet(
account=_ACCOUNT,
fee=_FEE,
sequence=_SEQUENCE,
signer_quorum=2,
signer_entries=signer_entries,
)
42 changes: 38 additions & 4 deletions xrpl/models/transactions/signer_list_set.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
"""Model for SignerListSet transaction type."""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Pattern

from typing_extensions import Final

from xrpl.models.nested_model import NestedModel
from xrpl.models.required import REQUIRED
from xrpl.models.transactions.transaction import Transaction
from xrpl.models.transactions.types import TransactionType
from xrpl.models.utils import require_kwargs_on_init

MAX_SIGNER_ENTRIES: Final[int] = 32
"""
Maximum number of signer entries allowed.

:meta private:
"""

HEX_WALLET_LOCATOR_REGEX: Final[Pattern[str]] = re.compile("[A-Fa-f0-9]{64}")
"""
Matches hex-encoded WalletLocator in the format allowed by XRPL.

:meta private:
"""


@require_kwargs_on_init
@dataclass(frozen=True)
Expand All @@ -30,6 +47,13 @@ class SignerEntry(NestedModel):
:meta hide-value:
"""

wallet_locator: Optional[str] = None
"""
An arbitrary 256-bit (32-byte) field that can be used to identify the signer, which
may be useful for smart contracts, or for identifying who controls a key in a large
organization.
"""


@require_kwargs_on_init
@dataclass(frozen=True)
Expand Down Expand Up @@ -78,11 +102,14 @@ def _get_errors(self: SignerListSet) -> Dict[str, str]:
"signer_quorum"
] = "`signer_quorum` must be greater than or equal to 0."

if len(self.signer_entries) < 1 or len(self.signer_entries) > 8:
if (
len(self.signer_entries) < 1
or len(self.signer_entries) > MAX_SIGNER_ENTRIES
):
errors["signer_entries"] = (
"`signer_entries` must have at least 1 member and no more than 8 "
"`signer_entries` must have at least 1 member and no more than {} "
"members. If this transaction is deleting the SignerList, then "
"this parameter must be omitted."
"this parameter must be omitted.".format(MAX_SIGNER_ENTRIES)
)
return errors

Expand All @@ -95,6 +122,13 @@ def _get_errors(self: SignerListSet) -> Dict[str, str]:
"The account submitting the transaction cannot appear in a "
"signer entry."
)
if signer_entry.wallet_locator is not None and not bool(
HEX_WALLET_LOCATOR_REGEX.fullmatch(signer_entry.wallet_locator)
):
errors["signer_entries"] = (
"A SignerEntry's wallet_locator must be a 256-bit (32-byte)"
"hexadecimal value."
)
account_set.add(signer_entry.account)
signer_weight_sum += signer_entry.signer_weight

Expand Down