diff --git a/network-interaction-sdk/LICENSE b/network-interaction-sdk/LICENSE new file mode 100644 index 0000000..24fb407 --- /dev/null +++ b/network-interaction-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 MultiversX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/network-interaction-sdk/README.md b/network-interaction-sdk/README.md new file mode 100644 index 0000000..055ffd8 --- /dev/null +++ b/network-interaction-sdk/README.md @@ -0,0 +1,49 @@ +# Specifications for mx-sdk-* libraries + +This repository contains specifications for the `mx-sdk-*` libraries. The specifications are written in a language-agnostic manner, and are meant to be implemented in multiple languages (e.g. Go, TypeScript, Python, Rust, etc.). + +## Structure + +- `sdk-core`: core components (address, transaction, etc.). +- `sdk-wallet`: core wallet components (generation, signing). +- `sdk-network-providers`: Network Provider (API, Gateway) components. + +Below, we add specific details for some of the most important packages and sub-components. + +### Transactions Factories + +These components are located in `sdk-core/transactions-factories` and are responsible with creating transactions for specific use cases. They are designed as _multi-factory_ classes, having methods that return a `Transaction` object constructed by following specific recipes (with respect to the Protocol). + +The methods are named in correspondence with the use cases they implement, e.g. `create_transaction_for_native_transfer()` or `create_transaction_for_new_delegation_contract()`. They return a `Transaction` (data transfer object), where `sender`, `receiver`, `value`, `data` and `gasLimit` are properly set (upon eventual computation, where applicable). + +Optionally, the implementing library can choose to return an object that isn't a complete representation of the `Transaction`, if desired. In this case, the library must name the incomplete representation `DraftTransaction`, and also must provide a direct conversion facility from `DraftTransaction` to `Transaction` - for example, a named constructor. See [transaction](sdk-core/transaction.md). + +## Guidelines + +### **`in-ifaces-out-concrete-types`** + +Generally speaking, it's recommended to receive input parameters as abstractions (interfaces) in the public API of the SDKs. This leads to an improved decoupling, and allows for easier type substitution (e.g. easier mocking, testing). + +Generally speaking, it's recommended to return concrete types in the public API of the SDKs. The client code is responsible with decoupling from unnecessary data and behaviour of returned objects (e.g. by using interfaces, on their side). The only notable exception to this is when working with factories (abstract or method factories) that should have the function output an interface type. For example, have a look over `(User|Validator)WalletProvider.generate_keypair()` - this method returns abstract types (interfaces). + +### **`pay-attention-to-types`** + + - For JavaScript / TypeScript, `bytes` should be `Uint8Array`. + +### **`follow-language-conventions`** + + - Make sure to follow the naming conventions of the language you're using, e.g. `snake_case` vs. `camelCase`. + - In the specs, interfaces are prefixed with `I`, simply to make them stand out. However, in the implementing libraries, this convention does not have to be applied. + - In `go`, the term `serialize` (whether it's part of a class name or a function name) can be replaced by `marshal`, since that is the convention. + - Errors should also follow the language convention - e.g. `ErrInvalidPublicKey` vs `InvalidPublicKeyError`. Should have the same error message in all implementing libraries, though. + +## **`any-object`** + +In the specs, `object` is used as a placeholder for any type of object. In the implementing libraries, this would be replaced with the most appropriate type. For example: + +- in Go: + - before Go 1.18: `map[string]interface{}` or directly `interface{}` (depending on the context) + - after Go 1.18: `map[string]any` or directly `any` (depending on the context) +- in Python: `dict` or `Any` +- in JavaScript / TypeScript: `object` or `any` + diff --git a/network-interaction-sdk/sdk-core/address.md b/network-interaction-sdk/sdk-core/address.md new file mode 100644 index 0000000..2a6759e --- /dev/null +++ b/network-interaction-sdk/sdk-core/address.md @@ -0,0 +1,99 @@ +## Address + +``` +class Address: + // Should also validate the length of the provided input. + // Can throw: + // - ErrInvalidPublicKey + constructor(public_key: bytes, hrp: string); + + // Named constructor + // Can throw: + // - ErrInvalidBech32Address + static new_from_bech32(value: string): Address; + + // Named constructor + // Can throw: + // - ErrInvalidHexString + static new_from_hex(value: string, hrp: string): Address; + + // Returns the address as a string (bech32). + // Name should be adjusted to match the language's convention. + to_bech32(): string; + + // Returns the address as a string (hex). + // Name should be adjusted to match the language's convention. + to_hex(): string; + + // Returns the underlying public key. + get_public_key(): bytes; + + // Returns the human-readable part of the address. + get_hrp(): string; + + // Returns true if the address is a smart contract address. + is_smart_contract(): bool; +``` + +Example of usage: + +``` +address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") + +print("Address (bech32-encoded)", address.bech32()) +print("Public key (hex-encoded):", address.hex()) +``` + +## AddressFactory + +``` +class AddressFactory: + constructor(hrp: string = "erd"); + + // Creates an address from a bech32 string. + create_from_bech32(value: string): Address; + + // Creates an address from a public key. + create_from_public_key(public_key: bytes): Address; + + // Creates an address from a hex string. + create_from_hex(value: string): Address; +``` + +Example of usage: + +``` + +factory = AddressFactory("erd") + +address = factory.create_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +address = factory.create_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +address = factory.create_from_public_key(bytes.fromhex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1")) +``` + +## AddressComputer + +``` +class AddressComputer: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + + // Note that the first input parameter is received as an interface, but the return value is a concrete type (see guidelines). + compute_contract_address(deployer: IAddress, deployment_nonce: number): Address; + + // The number of shards (necessary for computing the shard ID) would be received as a constructor parameter - constructor is not captured by specs. + get_shard_of_address(address: IAddress): number; +``` + +Above, `IAddress` should satisfy: + +``` +get_public_key(): bytes; +get_hrp(): string; +``` + +OR, perhaps, it should simply satisfy: + +``` +// Name should be adjusted to match the language's convention. +to_bech32(): string; +``` diff --git a/network-interaction-sdk/sdk-core/amount.md b/network-interaction-sdk/sdk-core/amount.md new file mode 100644 index 0000000..9291a70 --- /dev/null +++ b/network-interaction-sdk/sdk-core/amount.md @@ -0,0 +1,54 @@ +## Amount + +Generally speaking, the `Amount` type should be: + - `int` in Python + - `big.Int` or `string` in Go + - `BigNumber.Value` (which includes `string`) in JavaScript + +The implementing library can define type aliases, if desired, for example: + +```Python +Amount = int +``` + +```Go +type Amount = big.Int +``` + +```JavaScript +type Amount = BigNumber.Value +``` + +Furthermore, the implementing library is free to define a wrapper class or structure. This isn't a requirement though. Example: + +``` +class Amount: + value: (big.Int|bigNumber|string) + to_string(): string +``` + +Within the specs, we use `Amount` and we mean `(bigNumber|string)`. Or, to put it differently, `(Go[big.Int]|Python[int]|JavaScript[BigNumber.Value|string])`. + +**Important:** + - the value of an `Amount` is **always expressed in atomic units**. For example, to represent 1 EGLD, the value of `Amount` should be `1000000000000000000`, whether it's a Go `big.Int`, a Python `int`, a `string` etc. + - `Amount` **must not be concerned** with the number of decimals of the token. Just as the Protocol itself isn't concerned with this. + +## AmountInUnits + +Generally speaking, the `AmountInUnits` type should always be a `string`, so that **computations based on `AmountInUnits` are discouraged**. This type is meant to be used in the higher layers of an application, for example, on display purposes. + +The implementing library can define type aliases or wrapper classes (structures), if desired. See above. + +## AmountComputer + +``` +class AmountComputer: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // For example, the constructor can be parametrized with the decimals separator (e.g. "." or ",") and with the number of decimals of the native token (18). + + amount_to_units(amount: Amount, num_decimals: number): AmountInUnits; + units_to_amount(units: AmountInUnits, num_decimals: number): Amount; + + native_amount_to_units(amount: Amount): AmountInUnits; + native_units_to_amount(units: AmountInUnits): Amount; +``` diff --git a/network-interaction-sdk/sdk-core/message.md b/network-interaction-sdk/sdk-core/message.md new file mode 100644 index 0000000..1464f17 --- /dev/null +++ b/network-interaction-sdk/sdk-core/message.md @@ -0,0 +1,26 @@ +## Message + +``` +dto Message: + data: bytes; + signature: bytes; +``` + +## MessageComputer + +``` +class MessageComputer: + compute_bytes_for_signing(message: Message): bytes; +``` + +Reference implementation of `compute_bytes_for_signing`: + +``` +compute_bytes_for_signing(message: Message): + PREFIX = bytes.fromhex("17456c726f6e64205369676e6564204d6573736167653a0a") + size = length of message.data, encoded as a string + content = concat(PREFIX, size, message.data) + content_hash = keccak(digest_bits=256) + + return content_hash +``` diff --git a/network-interaction-sdk/sdk-core/tokens.md b/network-interaction-sdk/sdk-core/tokens.md new file mode 100644 index 0000000..661cd52 --- /dev/null +++ b/network-interaction-sdk/sdk-core/tokens.md @@ -0,0 +1,68 @@ +## Token + +``` +dto Token: + // E.g. "FOO-abcdef", "EGLD". + identifier: string; + nonce: number; +``` + +## TokenComputer + +``` +class TokenComputer: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + + // Returns token.nonce == 0. + is_fungible(token: Token): boolean; + + // Given "FOO-abcdef-0a" returns 10. + // Can throw: + // - ErrInvalidTokenIdentifier + extract_nonce_from_extended_identifier(extended_identifier: string): number; + + // Given "FOO-abcdef-0a" returns "FOO-abcdef". + // Can throw: + // - ErrInvalidTokenIdentifier + extract_identifier_from_extended_identifier(extended_identifier: string): string; + + // Given "FOO-abcdef-0a" returns "FOO". + // Given "FOO-abcdef" returns "FOO". + // Can throw: + // - ErrInvalidTokenIdentifier + extract_ticker_from_identifier(identifier: string): string; + + // Optionally, the implementing library can define a method for parsing all token parts at once. + // The return type can be a tuple or the struct `TokenIdentifierParts` (see below). + // Can throw: + // - ErrInvalidTokenIdentifier + parse_extended_identifier_parts(identifier: string): TokenIdentifierParts; + + // Given "FOO-abcdef" and 10 returns "FOO-abcdef-0a". If nonce is 0, returns "FOO-abcdef". + // For example, useful when preparing the parameters for querying token data from a network provider. + compute_extended_identifier_from_identifier_and_nonce(identifier: string, nonce: number): string; + + // Optional, if `parse_extended_identifier_parts()` is also implemented. + // Pick one of the following (if the language does not support overloading): + compute_extended_identifier_from_parts(ticker: string, random_sequence: string, nonce: number): string; + compute_extended_identifier_from_parts(parts: TokenIdentifierParts): string; +``` + +An optional structure, if the implementing library defines a `parse_extended_identifier_parts` method: + +``` +dto TokenIdentifierParts: + ticker: string; + random_sequence: string; + nonce: number; +``` + +## TokenTransfer + +``` +dto TokenTransfer: + token: Token; + + // Always in atomic units, e.g. for transferring 1.000000 "USDC-c76f1f", it must be "1000000". + amount: Amount; +``` diff --git a/network-interaction-sdk/sdk-core/transaction.md b/network-interaction-sdk/sdk-core/transaction.md new file mode 100644 index 0000000..e95d17d --- /dev/null +++ b/network-interaction-sdk/sdk-core/transaction.md @@ -0,0 +1,49 @@ +## Transaction + +``` +dto Transaction: + sender: string; + receiver: string; + gasLimit: uint32; + chainID: string; + + nonce?: uint64; + value?: Amount; + senderUsername?: string; + receiverUsername?: string; + gasPrice?: uint32; + + data?: bytes; + version?: uint32; + options?: uint32; + guardian?: string; + + signature: bytes; + guardianSignature?: bytes; + + // Optional named constructor, if and only if the implementing library defines a `DraftTransaction`. + new_from_draft(draft: DraftTransaction): Transaction; +``` + +## DraftTransaction + +Optionally, if desired, the implementing library can also define an incomplete representation of the transaction, to be used as return type for the **transaction factories**. See [README](../README.md), instead of the `Transaction` type. + +``` +dto DraftTransaction: + sender: string; + receiver: string; + value?: string; + data?: bytes; + gasLimit: uint32; + chainID: string; // The chain ID from the factory's config (received in the constructor) should be used +``` + +## TransactionComputer + +``` +class TransactionComputer: + compute_transaction_fee(transaction: Transaction, network_config: INetworkConfig): Amount; + compute_bytes_for_signing(transaction: Transaction): bytes; + compute_transaction_hash(transaction: Transaction): bytes; +``` diff --git a/network-interaction-sdk/sdk-core/transactions-factories/delegation_transactions_factory.md b/network-interaction-sdk/sdk-core/transactions-factories/delegation_transactions_factory.md new file mode 100644 index 0000000..52aa0d7 --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-factories/delegation_transactions_factory.md @@ -0,0 +1,121 @@ +## DelegationTransactionsFactory + +``` +class DelegationTransactionsFactory: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor should be parametrized with a configuration object which defines entries such as: + // "minGasLimit", "gasLimitPerByte", gas limit for specific operations etc. (e.g. "gasLimitForStaking"). + + create_transaction_for_new_delegation_contract({ + sender: IAddress; + totalDelegationCap: Amount; + service_fee: number; + amount: Amount; + }): Transaction; + + create_transaction_for_adding_nodes({ + sender: IAddress; + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + signedMessages: List[bytes]; + }): Transaction; + + create_transaction_for_removing_nodes({ + sender: IAddress, + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + }): Transaction; + + create_transaction_for_staking_nodes({ + sender: IAddress, + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + }): Transaction; + + create_transaction_for_unbonding_nodes({ + sender: IAddress, + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + }): Transaction; + + create_transaction_for_unstaking_nodes({ + sender: IAddress, + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + }): Transaction; + + create_transaction_for_unjailing_nodes({ + sender: IAddress, + delegationContract: IAddress; + publicKeys: List[IPublicKey]; + }): Transaction; + + create_transaction_for_changing_service_fee({ + sender: IAddress; + delegationContract: IAddress; + serviceFee: int; + }): Transaction; + + create_transaction_for_modifying_delegation_cap({ + sender: IAddress; + delegationContract: IAddress; + delegationCap: int; + }): Transaction; + + create_transaction_for_setting_automatic_activation({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_unsetting_automatic_activation({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_setting_cap_check_on_redelegate_rewards({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_unsetting_cap_check_on_redelegate_rewards({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_setting_metadata({ + sender: IAddress; + delegationContract: IAddress; + name: str; + website: str; + identifier: str; + }): Transaction; + + create_transaction_for_delegating({ + sender: IAddress; + delegationContract: IAddress; + amount: Amount; + }): Transaction; + + create_transaction_for_claiming_rewards({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_redelegating_rewards({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + create_transaction_for_undelegating({ + sender: IAddress; + delegationContract: IAddres; + amount: Amount; + }): Transaction; + + create_transaction_for_withdrawing({ + sender: IAddress; + delegationContract: IAddress; + }): Transaction; + + ... +``` diff --git a/network-interaction-sdk/sdk-core/transactions-factories/relayed_transactions_factory.md b/network-interaction-sdk/sdk-core/transactions-factories/relayed_transactions_factory.md new file mode 100644 index 0000000..0ba2f14 --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-factories/relayed_transactions_factory.md @@ -0,0 +1,21 @@ +## RelayedTransactionsFactory + +class RelayedTransactionsFactory: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor should be parametrized with a configuration object which defines entries such as: + // "minGasLimit", "gasLimitPerByte", gas limit for specific operations etc. + + // Each implementation is responsible to check that each inner transaction is signed. + // Can throw InvalidInnerTransactionError + + create_relayed_v1_transaction({ + inner_transaction: ITransaction; + relayer_address: IAddress; + }): Transaction; + + // can throw InvalidInnerTransactionError if inner_transaction.gas_limit != 0 + create_relayed_v2_transaction({ + inner_transaction: ITransaction; + inner_transaction_gas_limit: uint32; + relayer_address: IAddress; + }); diff --git a/network-interaction-sdk/sdk-core/transactions-factories/smart_contract_transactions_factory.md b/network-interaction-sdk/sdk-core/transactions-factories/smart_contract_transactions_factory.md new file mode 100644 index 0000000..66e7abf --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-factories/smart_contract_transactions_factory.md @@ -0,0 +1,48 @@ +## SmartContractTransactionsFactory + +``` +class SmartContractTransactionsFactory: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor should be parametrized with a configuration object which defines entries such as: + // "minGasLimit", "gasLimitPerByte" etc. + // The constructor may also be parametrized with an `Abi` or `Codec` instance (implementation detail), necessary to encode contract call arguments. + + create_transaction_for_deploy({ + sender: IAddress; + bytecode: bytes OR bytecodePath: Path; + arguments: List[object] = []; + native_transfer_amount: Amount = 0; + isUpgradeable: bool = True; + isReadable: bool = True; + isPayable: bool = False; + isPayableBySC: bool = True; + gasLimit: uint32; + }): Transaction; + + create_transaction_for_execute({ + sender: IAddress; + contract: IAddress; + // If "function" is a reserved word in the implementing language, it should be replaced with a different name (e.g. "func" or "functionName"). + function: string; + arguments: List[object] = []; + native_transfer_amount: Amount = 0; + token_transfers: List[TokenTransfer] = []; + gasLimit: uint32; + }): Transaction; + + create_transaction_for_upgrade({ + sender: IAddress; + contract: IAddress; + bytecode: bytes OR bytecodePath: Path; + arguments: List[object] = []; + native_transfer_amount: Amount = 0; + isUpgradeable: bool = True; + isReadable: bool = True; + isPayable: bool = False; + isPayableBySC: bool = True; + gasLimit: uint32; + }): Transaction; + + ... + +``` diff --git a/network-interaction-sdk/sdk-core/transactions-factories/token_management_transactions_factory.md b/network-interaction-sdk/sdk-core/transactions-factories/token_management_transactions_factory.md new file mode 100644 index 0000000..68ad889 --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-factories/token_management_transactions_factory.md @@ -0,0 +1,186 @@ +## TokenManagementTransactionsFactory + +A class that provides methods for creating transactions for token management operations. + +``` +class TokenManagementTransactionsFactory: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor should be parametrized with a configuration object which defines entries such as: + // "minGasLimit", "gasLimitPerByte", "issueCost", gas limit for specific operations etc. (e.g. "gasLimitForSettingSpecialRole"). + + create_transaction_for_issuing_fungible({ + sender: IAddress; + tokenName: string; + tokenTicker: string; + initialSupply: int; + numDecimals: int; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): Transaction; + + create_transaction_for_issuing_semi_fungible({ + sender: IAddress; + tokenName: string; + tokenTicker: string; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): Transaction; + + create_transaction_for_issuing_non_fungible({ + sender: IAddress; + tokenName: string; + tokenTicker: string; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): Transaction; + + create_transaction_for_registering_meta_esdt({ + sender: IAddress; + tokenName: string; + tokenTicker: string; + numDecimals: number; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferNFTCreateRole: boolean; + canChangeOwner: boolean; + canUpgrade: boolean; + canAddSpecialRoles: boolean; + }): Transaction; + + create_transaction_for_registering_and_setting_roles({ + sender: IAddress; + tokenName: string; + tokenTicker: string; + tokenType: RegisterAndSetAllRolesTokenType; + numDecimals: number; + }): Transaction; + + create_transaction_for_setting_burn_role_globally({ + sender: IAddress; + tokenIdentifier: string; + }): Transaction; + + create_transaction_for_unsetting_burn_role_globally({ + sender: IAddress; + tokenIdentifier: string; + }): Transaction; + + create_transaction_for_setting_special_role_on_fungible_token({ + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleLocalMint: boolean; + addRoleLocalBurn: boolean; + }): Transaction; + + create_transaction_for_setting_special_role_on_semi_fungible_token({ + sender: IAddress; + user: IAddress; + tokenIdentifier: string; + addRoleNFTCreate: boolean; + addRoleNFTBurn: boolean; + addRoleNFTAddQuantity: boolean; + addRoleESDTTransferRole: boolean; + }): Transaction; + + create_transaction_for_setting_special_role_on_non_fungible_token({ + sender: IAddress; + user: IAddress; + tokenIdentifier: str; + addRoleNftCreate: bool; + addRoleNftBurn: bool; + addRoleNftUpdate_attributes: bool; + addRoleNftAddUri: bool; + addRoleESDTTransferRole: bool; + }): Transaction; + + create_transaction_for_creating_nft({ + sender: IAddress; + tokenIdentifier: str; + initialQuantity: int; + name: str; + royalties: int; + hash: str; + attributes: bytes; + uris: List[str]; + }): Transaction; + + create_transaction_for_pausing({ + sender: IAddress; + tokenIdentifier: str; + }): Transaction; + + create_transaction_for_unpausing({ + sender: IAddress; + tokenIdentifier: str; + }): Transaction; + + create_transaction_for_freezing({ + sender: IAddress; + user: IAddress; + tokenIdentifier: str; + }): Transaction; + + create_transaction_for_unfreezing({ + sender: IAddress; + user: IAddress; + tokenIdentifier: str; + }): Transaction; + + create_transaction_for_wiping({ + sender: IAddress; + user: IAddress; + tokenIdentifier: str; + }): Transaction; + + create_transaction_for_local_minting({ + sender: IAddress; + tokenIdentifier: str; + supplyToMint: int; + }): Transaction; + + create_transaction_for_local_burning({ + sender: IAddress; + tokenIdentifier: str; + supplyToBurn: int; + }): Transaction; + + create_transaction_for_updating_attributes({ + sender: IAddress; + tokenIdentifier: str; + tokenNonce: int; + attributes: bytes; + }): Transaction; + + create_transaction_for_adding_quantity({ + sender: IAddress; + tokenIdentifier: str; + tokenNonce: int; + quantityToAdd: int; + }): Transaction; + + create_transaction_for_burning_quantity({ + sender: IAddress; + tokenIdentifier: str; + tokenNonce: int; + quantityToBurn: int; + }): Transaction; + + ... +``` diff --git a/network-interaction-sdk/sdk-core/transactions-factories/transfer_transactions_factory.md b/network-interaction-sdk/sdk-core/transactions-factories/transfer_transactions_factory.md new file mode 100644 index 0000000..94f37dd --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-factories/transfer_transactions_factory.md @@ -0,0 +1,27 @@ +## TransferTransactionsFactory + +A class that provides methods for creating transactions for transfers (native or ESDT). + +``` +class TransferTransactionsFactory: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor should be parametrized with a configuration object which defines entries such as: + // "minGasLimit", "gasLimitPerByte", "issueCost", gas limit for specific operations etc. (e.g. "gasLimitForESDTTransfer"). + + create_transaction_for_native_token_transfer({ + sender: IAddress; + receiver: IAddress; + native_amount: Amount; + }): Transaction; + + // Can throw: + // - ErrBadArguments + // + // If multiple transfers are specified, a multi-ESDT transfer transaction should be created. + // Bad usage should be reported. + create_transaction_for_esdt_token_transfer({ + sender: IAddress; + receiver: IAddress; + token_transfers: TokenTransfer[]; + }): Transaction; +``` diff --git a/network-interaction-sdk/sdk-core/transactions-outcome-parsers/token_management_transactions_outcome_parser.md b/network-interaction-sdk/sdk-core/transactions-outcome-parsers/token_management_transactions_outcome_parser.md new file mode 100644 index 0000000..8c7629d --- /dev/null +++ b/network-interaction-sdk/sdk-core/transactions-outcome-parsers/token_management_transactions_outcome_parser.md @@ -0,0 +1 @@ +// TBD: will be defined in a future PR. diff --git a/network-interaction-sdk/sdk-wallet/README.md b/network-interaction-sdk/sdk-wallet/README.md new file mode 100644 index 0000000..63c05a2 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/README.md @@ -0,0 +1,13 @@ +## Keystores + +Components in `sdk-wallet/keystore`, even if they do share a common purpose - storing secret keys - are not meant to be used interchangeably. Generally speaking, they do not (are not required to) share a common interface. If needed (for exotic use-cases), client code can design customized adapters over these components in order to unify their interfaces (this is not covered by specs). + +**Implementation detail:** an instance of `EncryptedKeystore` (which is a wrapper over the well-known JSON wallet) holds decrypted data within its state. + +**Design detail:** components in `sdk-wallet/keystore` should not depend on `Address` within their public interface (though they are allowed to depend on it within their implementation). For example, the `export` functionality of `EncryptedKeystore` requires the functionality provided by `Address` (conversion from public key bytes to bech32 representation). + +### References: + - https://github.com/ethereumjs/keythereum + - https://github.com/ethereumjs/ethereumjs-wallet/blob/master/docs/classes/wallet.md + - https://github.com/multiversx/mx-sdk-js-wallet/blob/main/src/userWallet.ts + - https://github.com/multiversx/mx-sdk-py-wallet/blob/main/multiversx_sdk_wallet/user_wallet.py diff --git a/network-interaction-sdk/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md b/network-interaction-sdk/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md new file mode 100644 index 0000000..5766d1a --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md @@ -0,0 +1,8 @@ +## KeyPairBasedEncryptorDecryptor + +``` +class KeyPairBasedEncryptorDecryptor: + encrypt(data: bytes, recipient_public_key: IPublicKey, auth_secret_key: ISecretKey): PublicKeyEncryptedData; + + decrypt(data: PublicKeyEncryptedData, decryptor_secret_key: ISecretKey): bytes; +``` diff --git a/network-interaction-sdk/sdk-wallet/crypto/password_based_encryptor_decryptor.md b/network-interaction-sdk/sdk-wallet/crypto/password_based_encryptor_decryptor.md new file mode 100644 index 0000000..eb2989f --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/crypto/password_based_encryptor_decryptor.md @@ -0,0 +1,8 @@ +## PasswordBasedEncryptorDecryptor + +``` +class PasswordBasedEncryptorDecryptor: + encrypt(data: bytes, password: string): PasswordEncryptedData; + + decrypt(data: PasswordEncryptedData, password: string): bytes; +``` diff --git a/network-interaction-sdk/sdk-wallet/crypto/password_encrypted_data.md b/network-interaction-sdk/sdk-wallet/crypto/password_encrypted_data.md new file mode 100644 index 0000000..f184b14 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/crypto/password_encrypted_data.md @@ -0,0 +1,30 @@ +## PasswordEncryptedData + +``` +dto PasswordEncryptedData: + id: string; + version: number; + cipher: string; + ciphertext: string; + iv: string; + kdf: string; + kdfparams: object; + salt: string; + mac: string; + +// Example of class compatible with "PasswordEncryptedData.kdfparams". +// Can be anything, and it's specific to a "PasswordEncryptedData.kdf" (e.g. "scrypt"). +// EncryptorDecryptor.encrypt() puts this into "PasswordEncryptedData.kdfparams". +// EncryptorDecryptor.decrypt() interprets (possibly validates) it. +dto ScryptKeyDerivationParams: + // numIterations + n = 4096; + + // memFactor + r = 8; + + // pFactor + p = 1; + + dklen = 32; +``` diff --git a/network-interaction-sdk/sdk-wallet/crypto/public_key_encrypted_data.md b/network-interaction-sdk/sdk-wallet/crypto/public_key_encrypted_data.md new file mode 100644 index 0000000..29495ed --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/crypto/public_key_encrypted_data.md @@ -0,0 +1,16 @@ +## PublicKeyEncryptedData + +``` +dto PublicKeyEncryptedData: + nonce: string; + version: number; + cipher: string; + ciphertext: string; + mac: string; + identities: PublicKeyEncryptedDataIdentities; + +dto PublicKeyEncryptedDataIdentities: + recipient: string; + ephemeralPubKey: string; + originatorPubKey: string; +``` diff --git a/network-interaction-sdk/sdk-wallet/interfaces.md b/network-interaction-sdk/sdk-wallet/interfaces.md new file mode 100644 index 0000000..38427b4 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/interfaces.md @@ -0,0 +1,22 @@ +## Interfaces + +For languages that support **structural typing** (e.g. Go, Python, TypeScript), the following interfaces should not be _exported_, since we'd like to encourage client applications to define their own interfaces, as needed. + +For languages that only support **nominal typing** (e.g. C#), these interfaces can be _exported_. + +``` +interface IWalletProvider: + generate_keypair(): (ISecretKey, IPublicKey) + sign(data: bytes, secret_key: ISecretKey): bytes + verify(data: bytes, signature: bytes, public_key: IPublicKey): bool + create_secret_key_from_bytes(data: bytes): ISecretKey + create_public_key_from_bytes(data: bytes): IPublicKey + compute_public_key_from_secret_key(secret_key: ISecretKey): IPublicKey +``` + +``` +interface IMnemonicComputer: + generate_mnemonic(): Mnemonic + validate_mnemonic(mnemonic: Mnemonic): bool + derive_secret_key_from_mnemonic(mnemonic: Mnemonic, address_index: int, passphrase: string): ISecretKey +``` diff --git a/network-interaction-sdk/sdk-wallet/keystores/encrypted_keystore.md b/network-interaction-sdk/sdk-wallet/keystores/encrypted_keystore.md new file mode 100644 index 0000000..8238e8e --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/keystores/encrypted_keystore.md @@ -0,0 +1,120 @@ +## EncryptedKeystore + +``` +class EncryptedKeystore: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + + // Named constructor + static new_from_secret_key(secret_key: ISecretKey): EncryptedKeystore + + // Named constructor + // Below, "wallet_provider" should implement "derive_secret_key_from_mnemonic()". + // Advice: in the implementation all the parameters will be held as instance state (private fields). + static new_from_mnemonic(wallet_provider: IWalletProvider, mnemonic: Mnemonic): EncryptedKeystore + + // Importing "constructor" + static import_from_object(wallet_provider: IWalletProvider, object: KeyfileObject, password: string): EncryptedKeystore + + // Importing "constructor" + static import_from_file(wallet_provider: IWalletProvider, path: Path, password: string): EncryptedKeystore + + // When kind == 'secretKey', only index == 0 and passphrase == "" is supported. + // When kind == 'mnemonic', secret key derivation happens under the hood. + // Below, "passphrase" is the bip39 passphrase required to derive a secret key from a mnemonic (by default, it should be an empty string). + get_secret_key(index: int, passphrase: string): ISecretKey + + // Can throw: + // - ErrMnemonicNotAvailable + // + // Returns the mnemonic used to create the keystore (if available, i.e. if kind == 'mnemonic'). + // This function is useful for UX flows where the application has to display the mnemonic etc. + get_mnemonic(): Mnemonic + + export_to_object(password: string, address_hrp: string): KeyfileObject + + export_to_file(path: Path, password: string, address_hrp: string) +``` + +``` +dto KeyfileObject: + version: number; + + // "secretKey|mnemonic" + kind: string; + + // a GUID + id: string + + // hex representation of the address + address: string + + // bech32 representation of the address + bech32: string + + crypto: { + // PasswordEncryptedData.ciphertext + ciphertext: string; + + cipherparams: { + // PasswordEncryptedData.iv + iv: string; + }; + + // PasswordEncryptedData.cipher + cipher: string; + + // PasswordEncryptedData.kdf + kdf: string; + kdfparams: { + // PasswordEncryptedData.kdfparams.n + n: number; + + // PasswordEncryptedData.kdfparams.r + r: number; + + // PasswordEncryptedData.kdfparams.p + p: number; + + // PasswordEncryptedData.kdfparams.dklen + dklen: number; + + // PasswordEncryptedData.salt + salt: string; + }; + + // PasswordEncryptedData.mac + mac: string; + }; +``` + +## Examples of usage + +Create a new JSON keystore using a new mnemonic: + +``` +provider = new UserWalletProvider() +mnemonic = provider.generate_mnemonic() +keystore = EncryptedKeystore.new_from_mnemonic(provider, mnemonic) +keystore.export_to_file("file.json", "password", "erd") +``` + +Iterating over the first 3 accounts: + +``` +provider = new UserWalletProvider() +keystore = EncryptedKeystore.import_from_file(provider, "file.json", "password") + +for i in [0, 1, 2]: + secret_key = keystore.get_secret_key(i, "") + public_key = provider.compute_public_key_from_secret_key(secret_key) + address = new Address(public_key, "erd") + print("Address", i, address.bech32()) +``` + +Changing the password of an existing keystore: + +``` +provider = new UserWalletProvider() +keystore = EncryptedKeystore.import_from_file(provider, "file.json", "password") +keystore.export_to_file("file.json", "new_password", "erd") +``` diff --git a/network-interaction-sdk/sdk-wallet/keystores/pem_keystore.md b/network-interaction-sdk/sdk-wallet/keystores/pem_keystore.md new file mode 100644 index 0000000..71a1c3b --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/keystores/pem_keystore.md @@ -0,0 +1,53 @@ +## PEMStore + +``` +class PEMKeystore: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + + // Named constructor + static new_from_secret_key(wallet_provider: IWalletProvider, secret_key: ISecretKey): PEMKeystore + + // Named constructor + static new_from_secret_keys(wallet_provider: IWalletProvider, secret_keys: ISecretKey[]): PEMKeystore + + // Importing "constructor" + static import_from_text(wallet_provider: IWalletProvider, text: string): PEMKeystore + + // Importing "constructor" + static import_from_file(wallet_provider: IWalletProvider, path: Path): PEMKeystore + + get_secret_key(index: int): ISecretKey + + get_number_of_secret_keys(): number + + export_to_text(address_hrp: string): string + + export_to_file(path: Path, address_hrp: string) +``` + +## Examples of usage + +Creating a PEM file to hold three newly created secret keys. + +``` +provider = new UserWalletProvider() +sk1, _ = provider.generate_keypair() +sk2, _ = provider.generate_keypair() +sk3, _ = provider.generate_keypair() + +keystore = PEMKeystore.new_from_secret_keys(provider, [sk1, sk2, sk3]) +keystore.export_to_file(Path("file.pem"), "erd") +``` + +Iterating over the accounts in a PEM file. + +``` +provider = new UserWalletProvider() +keystore = PEMKeystore.import_from_file(provider, Path("file.pem")) + +for i in range(keystore.get_number_of_secret_keys()): + sk = keystore.get_secret_key(i) + pk = provider.compute_public_key_from_secret_key(sk) + address = new Address(public_key, "erd") + print("Address", i, address.bech32()) +``` diff --git a/network-interaction-sdk/sdk-wallet/mnemonic.md b/network-interaction-sdk/sdk-wallet/mnemonic.md new file mode 100644 index 0000000..84e55ed --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/mnemonic.md @@ -0,0 +1,60 @@ +## Mnemonic + +This component allows one to load / parse an existing mnemonic. + +``` +class Mnemonic: + // At least one of the following constructors should be implemented. + // The constructor(s) should also trim whitespace. + constructor(text: string); + constructor(words: string[]); + + // Alternatively, named constructors can be used: + static newfromText(text: string): Mnemonic; + static newfromWords(words: string[]): Mnemonic; + + // Gets the mnemonic words. + getWords(): string[]; + + // Returns the mnemonic as a string. + toString(): string; +} +``` + +## MnemonicComputer + +Encapsulates logic for generating and validating mnemonics and for deriving secret keys from mnemonics (i.e. BIP39). + +``` +class MnemonicComputer implements IMnemonicComputer: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + + // Should not throw. + generate_mnemonic(): Mnemonic + + // Should not throw. + validate_mnemonic(mnemonic: Mnemonic): bool + + // Can throw: + // - ErrInvalidMnemonic + // Below, "passphrase" is the optional bip39 passphrase used to derive a secret key from a mnemonic. + // Reference: https://en.bitcoin.it/wiki/Seed_phrase#Two-factor_seed_phrases + derive_secret_key_from_mnemonic(mnemonic: Mnemonic, address_index: int, passphrase: string = ""): ISecretKey +``` + +## Examples of usage + +Creating a new mnemonic and deriving the first secret key. + +``` +computer = new MnemonicComputer() +provider = new UserWalletProvider() +mnemonic = computer.generate_mnemonic() +print(mnemonic.toString()) + +mnemonic = Mnemonic.newfromText("...") +sk = computer.derive_secret_key_from_mnemonic(mnemonic, address_index=0 , passphrase="") +pk = provider.compute_public_key_from_secret_key(sk) +address = new Address(public_key, "erd") +print(address.bech32()) +``` diff --git a/network-interaction-sdk/sdk-wallet/public_key.md b/network-interaction-sdk/sdk-wallet/public_key.md new file mode 100644 index 0000000..54d9298 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/public_key.md @@ -0,0 +1,8 @@ +## PublicKey + +``` +interface IPublicKey: + get_bytes(): bytes +``` + +In order to create and handle public keys, use the methods of the `(User|Validator)WalletProvider`. diff --git a/network-interaction-sdk/sdk-wallet/secret_key.md b/network-interaction-sdk/sdk-wallet/secret_key.md new file mode 100644 index 0000000..4c2a7ad --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/secret_key.md @@ -0,0 +1,8 @@ +## SecretKey + +``` +interface ISecretKey: + get_bytes(): bytes +``` + +In order to create and handle secret keys, use the methods of the `(User|Validator)WalletProvider`. diff --git a/network-interaction-sdk/sdk-wallet/user_wallet_provider.md b/network-interaction-sdk/sdk-wallet/user_wallet_provider.md new file mode 100644 index 0000000..be0cd34 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/user_wallet_provider.md @@ -0,0 +1,31 @@ +## UserWalletProvider + +``` +class UserWalletProvider implements IWalletProvider: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // For example, the constructor can be parametrized with underlying, more low-level crypto components, if applicable. + + // Should not throw. + generate_keypair(): (ISecretKey, IPublicKey) + + // Can throw: + // - ErrInvalidSecretKey + sign(data: bytes, secret_key: ISecretKey): bytes + + // Can throw: + // - ErrInvalidPublicKey + // - ErrInvalidSignature + verify(data: bytes, signature: bytes, public_key: IPublicKey): bool + + // Can throw: + // - ErrInvalidSecretKeyBytes + create_secret_key_from_bytes(data: bytes): ISecretKey + + // Can throw: + // - ErrInvalidPublicKeyBytes + create_public_key_from_bytes(data: bytes): IPublicKey + + // Can throw: + // - ErrInvalidSecretKey + compute_public_key_from_secret_key(secret_key: ISecretKey): IPublicKey +``` diff --git a/network-interaction-sdk/sdk-wallet/validator_wallet_provider.md b/network-interaction-sdk/sdk-wallet/validator_wallet_provider.md new file mode 100644 index 0000000..9b52937 --- /dev/null +++ b/network-interaction-sdk/sdk-wallet/validator_wallet_provider.md @@ -0,0 +1,31 @@ +## ValidatorWalletProvider + +``` +class ValidatorWalletProvider implements IWalletProvider: + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // For example, the constructor can be parametrized with underlying, more low-level crypto components, if applicable. + + // Should not throw. + generate_keypair(): (ISecretKey, IPublicKey) + + // Can throw: + // - ErrInvalidSecretKey + sign(data: bytes, secret_key: ISecretKey): bytes + + // Can throw: + // - ErrInvalidPublicKey + // - ErrInvalidSignature + verify(data: bytes, signature: bytes, public_key: IPublicKey): bool + + // Can throw: + // - ErrInvalidSecretKeyBytes + create_secret_key_from_bytes(data: bytes): ISecretKey + + // Can throw: + // - ErrInvalidPublicKeyBytes + create_public_key_from_bytes(data: bytes): IPublicKey + + // Can throw: + // - ErrInvalidSecretKey + compute_public_key_from_secret_key(secret_key: ISecretKey): IPublicKey +```