Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7915e05021 | |||
| dfc12b1aaa | |||
| 5ed97fb3c9 | |||
| 8e021a4419 | |||
| e5db7cb915 | |||
| 9bd03af3e9 | |||
| e8a026ef0b | |||
| 86755aa6ba | |||
| 77e0c6425e | |||
| 0373e2b02a | |||
| e8dd347186 | |||
| 4ebd1dd7f5 | |||
| 62ab760656 | |||
| 32de7efc32 | |||
| deae210b82 | |||
| 5b2b45a6eb | |||
| 896a3e1be6 | |||
| 800390db85 | |||
| 1eaa13155c | |||
| fad3346096 | |||
| 150f832f8e | |||
| 202336b8a1 | |||
| f0e94f8e5e | |||
| 6cd00b8d10 | |||
| 534187cc8f | |||
| 25b4934f69 | |||
| 5ef7e24893 | |||
| 87ef46bc05 | |||
| f7bc5be8e4 | |||
| b309583886 | |||
| 245185710a | |||
| b7cfe31d72 | |||
| 68ca41a6be | |||
| 5621e7d22e | |||
| a1a5c7772d | |||
| b47deafc14 | |||
| cc6a6d8db2 | |||
| 5b28e24c17 | |||
| f8d68d8ef0 | |||
| a9d86508b5 | |||
| bb7fa587de | |||
| 6585732dfc | |||
| 2065e0fc17 | |||
| 3f7bdad59c | |||
| 6209e78c1e | |||
| 8e5062af96 | |||
| 496e642d7f | |||
| 07e18ec198 | |||
| d5953c28c1 | |||
| 3aa4b66588 | |||
| 005f0ce340 | |||
| 8d1d025fa2 | |||
| 1d53a2f954 | |||
| 966d123608 | |||
| 963d55273f | |||
| 6872d7bf77 | |||
| 6fe93bcda0 | |||
| 78d568e04e | |||
| 796f5a678a | |||
| 0dfd1cca44 | |||
| eacefd3697 | |||
| 424c25768c | |||
| e460c1700e | |||
| 9935c99d41 | |||
| 2c4aee63bf | |||
| 0ebc395df9 | |||
| 1de3317e75 | |||
| 91c20af893 | |||
| 1365e2f246 | |||
| 3f4f76859b | |||
| 221e1870e5 | |||
| 9b36bccf0c | |||
| 80d7285497 | |||
| b94f2a483d | |||
| f64cfb4cd1 | |||
| 30e2f27c64 | |||
| 3113c1e9a7 | |||
| 1d8a931e0c | |||
| 48d0883b31 | |||
| e271370326 |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: 'Documentation'
|
||||
about: Suggest a fix or enhancement to the documentation or developer portal content
|
||||
title: "[DOCS]"
|
||||
labels: documentation
|
||||
assignees: mfahampshire
|
||||
|
||||
---
|
||||
|
||||
Is your issue either:
|
||||
- [ ] a fix to existing documentation (e.g. fixing a broken link or incorrect command)
|
||||
- [ ] an enhancement (e.g. adding a description for an undocumented feature)
|
||||
|
||||
Please briefly describe your issue:
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Upload nyxd to CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-nyxd:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/nyxd
|
||||
run: |
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p $OUTPUT_DIR
|
||||
echo $OUTPUT_DIR
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools git
|
||||
continue-on-error: true
|
||||
|
||||
- name: Update env variables to include go
|
||||
run: |
|
||||
sudo rm -rf /usr/local/go
|
||||
curl https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz | sudo tar -C/usr/local -zxvf -
|
||||
cat <<'EOF' >>$HOME/.profile
|
||||
export GOROOT=/usr/local/go
|
||||
export GOPATH=$HOME/go
|
||||
export GO111MODULE=on
|
||||
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
||||
EOF
|
||||
source $HOME/.profile
|
||||
|
||||
- name: Verify Go is installed
|
||||
run: go version
|
||||
|
||||
- name: Clone nyxd repo
|
||||
run: |
|
||||
git clone https://github.com/tommyv1987/nyxd
|
||||
cd nyxd
|
||||
git checkout release/v0.30.2
|
||||
|
||||
- name: Run nyxd
|
||||
run: |
|
||||
pwd
|
||||
cd nyxd && make build
|
||||
sleep 10
|
||||
ls /home/runner/work/nym/nym/nyxd/build
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/nyxd
|
||||
run: |
|
||||
cp /home/runner/work/nym/nym/nyxd/build/nyxd $OUTPUT_DIR
|
||||
WASMVM_SO=$(ldd /home/runner/work/nym/nym/nyxd/build/nyxd | grep "libwasm*" | awk '{ print $3 }')
|
||||
ls $WASMVM_SO
|
||||
sleep 3
|
||||
cp $(echo $WASMVM_SO) $OUTPUT_DIR
|
||||
|
||||
- name: Deploy nyxd to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "ci-builds/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -4,6 +4,46 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v1.1.19] (2023-05-16)
|
||||
|
||||
- nym-name-service endpoint in nym-api ([#3403])
|
||||
- Implement key storage for WASM client using IndexedDB (for browser) ([#3329])
|
||||
- Initial version of nym-name-service contract providing name aliases for nym-addresses ([#3274])
|
||||
- Update Cargo.lock ([#3410])
|
||||
|
||||
[#3403]: https://github.com/nymtech/nym/issues/3403
|
||||
[#3329]: https://github.com/nymtech/nym/issues/3329
|
||||
[#3274]: https://github.com/nymtech/nym/issues/3274
|
||||
[#3410]: https://github.com/nymtech/nym/pull/3410
|
||||
|
||||
## [v1.1.18] (2023-05-09)
|
||||
|
||||
- Implement heartbeat messages between socks5 proxy and network requester ([#3215])
|
||||
|
||||
[#3215]: https://github.com/nymtech/nym/issues/3215
|
||||
|
||||
## [v1.1.17] (2023-05-02)
|
||||
|
||||
- Add service-provider-directory-contract support to nym-cli ([#3334])
|
||||
- Start using the node-testing-utils (implemented in #3270) in nym-api Network monitor to simplify the logic there ([#3312])
|
||||
- Add service-provider-directory support to validator-client ([#3296])
|
||||
- Allow topology injection in our WASM client ('test my node' feature) ([#3270])
|
||||
- Expose service-provider-directory contract data in nym-api endpoints ([#3242])
|
||||
- Cache service provider contract in nym-api ([#3241])
|
||||
- Feature/1 1 17 docs ([#3370])
|
||||
- adding a test for SP endpoint ([#3367])
|
||||
- Feature/store cipher ([#3350])
|
||||
|
||||
[#3334]: https://github.com/nymtech/nym/issues/3334
|
||||
[#3312]: https://github.com/nymtech/nym/issues/3312
|
||||
[#3296]: https://github.com/nymtech/nym/issues/3296
|
||||
[#3270]: https://github.com/nymtech/nym/issues/3270
|
||||
[#3242]: https://github.com/nymtech/nym/issues/3242
|
||||
[#3241]: https://github.com/nymtech/nym/issues/3241
|
||||
[#3370]: https://github.com/nymtech/nym/pull/3370
|
||||
[#3367]: https://github.com/nymtech/nym/pull/3367
|
||||
[#3350]: https://github.com/nymtech/nym/pull/3350
|
||||
|
||||
## [v1.1.16] (2023-04-25)
|
||||
|
||||
- Explorer - Fix sorting function on Stake Saturation. It is currently working per page and not globally ([#3320])
|
||||
|
||||
Generated
+696
-575
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/name-service",
|
||||
"common/cosmwasm-smart-contracts/service-provider-directory",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
@@ -60,12 +61,14 @@ members = [
|
||||
"common/nymsphinx/forwarding",
|
||||
"common/nymsphinx/framing",
|
||||
"common/nymsphinx/params",
|
||||
"common/nymsphinx/routing",
|
||||
"common/nymsphinx/types",
|
||||
"common/pemstore",
|
||||
"common/socks5-client-core",
|
||||
"common/socks5/proxy-helpers",
|
||||
"common/socks5/requests",
|
||||
"common/statistics",
|
||||
"common/store-cipher",
|
||||
"common/task",
|
||||
"common/topology",
|
||||
"common/types",
|
||||
|
||||
@@ -99,6 +99,7 @@ CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
|
||||
VESTING_CONTRACT=$(CONTRACTS_OUT_DIR)/vesting_contract.wasm
|
||||
MIXNET_CONTRACT=$(CONTRACTS_OUT_DIR)/mixnet_contract.wasm
|
||||
SERVICE_PROVIDER_DIRECTORY_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_service_provider_directory.wasm
|
||||
NAME_SERVICE_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_name_service.wasm
|
||||
|
||||
wasm: wasm-build wasm-opt
|
||||
|
||||
@@ -109,6 +110,7 @@ wasm-opt:
|
||||
wasm-opt --disable-sign-ext -Os $(VESTING_CONTRACT) -o $(VESTING_CONTRACT)
|
||||
wasm-opt --disable-sign-ext -Os $(MIXNET_CONTRACT) -o $(MIXNET_CONTRACT)
|
||||
wasm-opt --disable-sign-ext -Os $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) -o $(SERVICE_PROVIDER_DIRECTORY_CONTRACT)
|
||||
wasm-opt --disable-sign-ext -Os $(NAME_SERVICE_CONTRACT) -o $(NAME_SERVICE_CONTRACT)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Misc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.16"
|
||||
version = "1.1.19"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.16"
|
||||
version = "1.1.19"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -34,7 +34,14 @@ import {
|
||||
StakeSaturationResponse,
|
||||
UnbondedMixnodeResponse,
|
||||
VestingAccountInfo,
|
||||
ContractState, VestingAccountsCoinPaged, VestingAccountsPaged, DelegationTimes, Delegations, Period, VestingAccountNode, DelegationBlock
|
||||
ContractState,
|
||||
VestingAccountsCoinPaged,
|
||||
VestingAccountsPaged,
|
||||
DelegationTimes,
|
||||
Delegations,
|
||||
Period,
|
||||
VestingAccountNode,
|
||||
DelegationBlock,
|
||||
} from '@nymproject/types';
|
||||
import QueryClient from './query-client';
|
||||
import SigningClient, { ISigningClient } from './signing-client';
|
||||
@@ -207,7 +214,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let mixNodes: UnbondedMixnodeResponse[] = [];
|
||||
const limit = 50;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedUnbondedMixnodesResponse = await this.client.getUnbondedMixNodes(
|
||||
this.mixnetContract,
|
||||
@@ -230,7 +237,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let mixNodes: MixNodeBond[] = [];
|
||||
const limit = 50;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedMixNodeBondResponse = await this.client.getMixNodeBonds(
|
||||
this.mixnetContract,
|
||||
@@ -252,7 +259,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let mixNodes: MixNodeDetails[] = [];
|
||||
const limit = 50;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedMixNodeDetailsResponse = await this.client.getMixNodesDetailed(
|
||||
this.mixnetContract,
|
||||
@@ -284,7 +291,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let delegations: Delegation[] = [];
|
||||
const limit = 250;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedMixDelegationsResponse = await this.client.getMixNodeDelegationsPaged(
|
||||
this.mixnetContract,
|
||||
@@ -307,7 +314,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let delegations: Delegation[] = [];
|
||||
const limit = 250;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedDelegatorDelegationsResponse = await this.client.getDelegatorDelegationsPaged(
|
||||
this.mixnetContract,
|
||||
@@ -330,7 +337,7 @@ export default class ValidatorClient implements INymClient {
|
||||
let delegations: Delegation[] = [];
|
||||
const limit = 250;
|
||||
let startAfter;
|
||||
for (; ;) {
|
||||
for (;;) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pagedResponse: PagedAllDelegationsResponse = await this.client.getAllDelegationsPaged(
|
||||
this.mixnetContract,
|
||||
@@ -518,11 +525,9 @@ export default class ValidatorClient implements INymClient {
|
||||
return (this.client as ISigningClient).updateContractStateParams(this.mixnetContract, newParams, fee, memo);
|
||||
}
|
||||
|
||||
|
||||
// VESTING
|
||||
// VESTING
|
||||
// TODO - MOVE TO A DIFFERENT FILE
|
||||
|
||||
|
||||
public async getVestingAccountsPaged(): Promise<VestingAccountsPaged> {
|
||||
return this.client.getVestingAccountsPaged(this.vestingContract);
|
||||
}
|
||||
@@ -608,9 +613,9 @@ export default class ValidatorClient implements INymClient {
|
||||
}
|
||||
|
||||
public async getDelegation(address: string, mix_id: number): Promise<DelegationBlock> {
|
||||
return this.client.getDelegation(this.vestingContract, address, mix_id );
|
||||
return this.client.getDelegation(this.vestingContract, address, mix_id);
|
||||
}
|
||||
|
||||
|
||||
public async getTotalDelegationAmount(address: string, mix_id: number, block_timestamp_sec: number): Promise<Coin> {
|
||||
return this.client.getTotalDelegationAmount(this.vestingContract, address, mix_id, block_timestamp_sec);
|
||||
}
|
||||
@@ -618,4 +623,10 @@ export default class ValidatorClient implements INymClient {
|
||||
public async getCurrentVestingPeriod(address: string): Promise<Period> {
|
||||
return this.client.getCurrentVestingPeriod(this.vestingContract, address);
|
||||
}
|
||||
|
||||
// SIMULATE
|
||||
|
||||
public async simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]) {
|
||||
return (this.client as SigningClient).simulateSend(signingAddress, from, to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,18 @@ import {
|
||||
RewardingParams,
|
||||
UnbondedMixnodeResponse,
|
||||
VestingAccountInfo,
|
||||
ContractState, VestingAccountsCoinPaged, VestingAccountsPaged, DelegationTimes, Delegations, Period, VestingAccountNode, DelegationBlock
|
||||
ContractState,
|
||||
VestingAccountsCoinPaged,
|
||||
VestingAccountsPaged,
|
||||
DelegationTimes,
|
||||
Delegations,
|
||||
Period,
|
||||
VestingAccountNode,
|
||||
DelegationBlock,
|
||||
} from '@nymproject/types';
|
||||
import NymApiQuerier from './nym-api-querier';
|
||||
import { makeBankMsgSend } from './utils';
|
||||
import { ISimulateClient } from './types/simulate';
|
||||
|
||||
// methods exposed by `SigningCosmWasmClient`
|
||||
export interface ICosmWasmSigning {
|
||||
@@ -148,7 +157,7 @@ export interface INymSigning {
|
||||
clientAddress: string;
|
||||
}
|
||||
|
||||
export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSigning {
|
||||
export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSigning, ISimulateClient {
|
||||
bondMixNode(
|
||||
mixnetContractAddress: string,
|
||||
mixNode: MixNode,
|
||||
@@ -511,11 +520,11 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
|
||||
);
|
||||
}
|
||||
|
||||
// vesting related
|
||||
// vesting related
|
||||
|
||||
getVestingAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsPaged> {
|
||||
return this.nyxdQuerier.getVestingAccountsPaged(vestingContractAddress);
|
||||
};
|
||||
}
|
||||
|
||||
getVestingAmountsAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsCoinPaged> {
|
||||
return this.nyxdQuerier.getVestingAmountsAccountsPaged(vestingContractAddress);
|
||||
@@ -569,7 +578,10 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
|
||||
return this.nyxdQuerier.getEndTime(vestingContractAddress, vestingAccountAddress);
|
||||
}
|
||||
|
||||
getOriginalVestingDetails(vestingContractAddress: string, vestingAccountAddress: string): Promise<OriginalVestingResponse> {
|
||||
getOriginalVestingDetails(
|
||||
vestingContractAddress: string,
|
||||
vestingAccountAddress: string,
|
||||
): Promise<OriginalVestingResponse> {
|
||||
return this.nyxdQuerier.getOriginalVestingDetails(vestingContractAddress, vestingAccountAddress);
|
||||
}
|
||||
|
||||
@@ -589,7 +601,11 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
|
||||
return this.nyxdQuerier.getGateway(vestingContractAddress, address);
|
||||
}
|
||||
|
||||
getDelegationTimes(vestingContractAddress: string, mix_id: number, delegatorAddress: string): Promise<DelegationTimes> {
|
||||
getDelegationTimes(
|
||||
vestingContractAddress: string,
|
||||
mix_id: number,
|
||||
delegatorAddress: string,
|
||||
): Promise<DelegationTimes> {
|
||||
return this.nyxdQuerier.getDelegationTimes(vestingContractAddress, mix_id, delegatorAddress);
|
||||
}
|
||||
|
||||
@@ -597,15 +613,38 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
|
||||
return this.nyxdQuerier.getAllDelegations(vestingContractAddress);
|
||||
}
|
||||
|
||||
getDelegation(vestingContractAddress: string, vestingAccountAddress: string, mix_id: number): Promise<DelegationBlock> {
|
||||
getDelegation(
|
||||
vestingContractAddress: string,
|
||||
vestingAccountAddress: string,
|
||||
mix_id: number,
|
||||
): Promise<DelegationBlock> {
|
||||
return this.nyxdQuerier.getDelegation(vestingContractAddress, vestingAccountAddress, mix_id);
|
||||
}
|
||||
|
||||
getTotalDelegationAmount(vestingContractAddress: string, vestingAccountAddress: string, mix_id: number, block_timestamp_sec: number): Promise<Coin> {
|
||||
return this.nyxdQuerier.getTotalDelegationAmount(vestingContractAddress, vestingAccountAddress, mix_id, block_timestamp_sec);
|
||||
getTotalDelegationAmount(
|
||||
vestingContractAddress: string,
|
||||
vestingAccountAddress: string,
|
||||
mix_id: number,
|
||||
block_timestamp_sec: number,
|
||||
): Promise<Coin> {
|
||||
return this.nyxdQuerier.getTotalDelegationAmount(
|
||||
vestingContractAddress,
|
||||
vestingAccountAddress,
|
||||
mix_id,
|
||||
block_timestamp_sec,
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentVestingPeriod(vestingContractAddress: string, address: string): Promise<Period> {
|
||||
return this.nyxdQuerier.getCurrentVestingPeriod(vestingContractAddress, address);
|
||||
}
|
||||
|
||||
// simulation
|
||||
|
||||
// TODO consider adding multipling factor
|
||||
|
||||
simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]) {
|
||||
const sendMsg = makeBankMsgSend(from, to, amount);
|
||||
return this.simulate(signingAddress, [sendMsg], 'simulate send tx');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import expect from 'expect';
|
||||
import ValidatorClient from '../..';
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// TODO: implement for QA with .env for mnemonics
|
||||
describe('Simualtions', () => {
|
||||
let client: ValidatorClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = await ValidatorClient.connect(
|
||||
process.env.mnemonic || '',
|
||||
process.env.rpcAddress || '',
|
||||
process.env.validatorAddress || '',
|
||||
process.env.prefix || '',
|
||||
process.env.mixnetContractAddress || '',
|
||||
process.env.vestingContractAddress || '',
|
||||
process.env.denom || '',
|
||||
);
|
||||
});
|
||||
|
||||
it('can simulate sending tokens', async () => {
|
||||
const res = await client.simulateSend(client.address, client.address, client.address, [
|
||||
{ amount: '400000', denom: 'unym' },
|
||||
]);
|
||||
|
||||
expect(typeof res).toBe('number');
|
||||
}).timeout(10000);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Coin } from '@cosmjs/proto-signing';
|
||||
|
||||
export interface ISimulateClient {
|
||||
simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]): Promise<number>;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ wee_alloc = { version = "0.4", optional = true }
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = true
|
||||
wasm-opt = false
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::{console_log, js_error, simple_js_error};
|
||||
use wasm_utils::{console_log, simple_js_error};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymClientTestRequest {
|
||||
@@ -142,20 +142,9 @@ impl WasmTopologyExt for Arc<ClientState> {
|
||||
return Err(WasmClientError::NonExistentMixnode { mixnode_identity }.into());
|
||||
};
|
||||
|
||||
let mut test_msgs = Vec::with_capacity(num_test_packets as usize);
|
||||
for i in 1..=num_test_packets {
|
||||
let msg = NodeTestMessage::new_mix(
|
||||
mix,
|
||||
i,
|
||||
num_test_packets,
|
||||
WasmTestMessageExt::new(test_id),
|
||||
);
|
||||
let serialized = match msg.as_bytes() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => return Err(js_error!("failed to serialize test message: {err}")),
|
||||
};
|
||||
test_msgs.push(serialized);
|
||||
}
|
||||
let ext = WasmTestMessageExt::new(test_id);
|
||||
let test_msgs = NodeTestMessage::mix_plaintexts(mix, num_test_packets, ext)
|
||||
.map_err(WasmClientError::from)?;
|
||||
|
||||
let mut updated = current_topology.clone();
|
||||
updated.set_mixes_in_layer(mix.layer.into(), vec![mix.to_owned()]);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tester::helpers::NodeTestResult;
|
||||
use crate::tester::NodeTestMessage;
|
||||
use crate::tester::helpers::{NodeTestResult, WasmTestMessageExt};
|
||||
use futures::StreamExt;
|
||||
use nym_node_tester_utils::receiver::{Received, ReceivedReceiver};
|
||||
use nym_node_tester_utils::processor::Received;
|
||||
use nym_node_tester_utils::receiver::ReceivedReceiver;
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
@@ -21,7 +21,7 @@ pub(crate) struct EphemeralTestReceiver<'a> {
|
||||
duplicate_acks: u32,
|
||||
|
||||
timeout_duration: Duration,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver>,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
|
||||
}
|
||||
|
||||
impl<'a> EphemeralTestReceiver<'a> {
|
||||
@@ -38,7 +38,7 @@ impl<'a> EphemeralTestReceiver<'a> {
|
||||
pub(crate) fn new(
|
||||
sent_packets: u32,
|
||||
expected_acks: HashSet<FragmentIdentifier>,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver>,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
EphemeralTestReceiver {
|
||||
@@ -53,23 +53,18 @@ impl<'a> EphemeralTestReceiver<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_next_received_packet(&mut self, packet: Option<Received>) -> bool {
|
||||
fn on_next_received_packet(&mut self, packet: Option<Received<WasmTestMessageExt>>) -> bool {
|
||||
let Some(received_packet) = packet else {
|
||||
// can't do anything more...
|
||||
console_error!("packet receiver has stopped processing results!");
|
||||
return true
|
||||
};
|
||||
match received_packet {
|
||||
Received::Message(msg) => match NodeTestMessage::try_recover(msg) {
|
||||
Ok(test_msg) => {
|
||||
if !self.received_valid_messages.insert(test_msg.msg_id) {
|
||||
self.duplicate_packets += 1;
|
||||
}
|
||||
Received::Message(msg) => {
|
||||
if !self.received_valid_messages.insert(msg.msg_id) {
|
||||
self.duplicate_packets += 1;
|
||||
}
|
||||
Err(err) => {
|
||||
console_warn!("failed to recover test message from received packet: {err}")
|
||||
}
|
||||
},
|
||||
}
|
||||
Received::Ack(frag_id) => {
|
||||
if self.expected_acks.contains(&frag_id) {
|
||||
if !self.received_valid_acks.insert(frag_id) {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// due to expansion of #[wasm_bindgen] macro on NodeTestResult
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
use nym_node_tester_utils::receiver::{Received, ReceivedReceiver};
|
||||
use nym_node_tester_utils::processor::Received;
|
||||
use nym_node_tester_utils::receiver::ReceivedReceiver;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -14,10 +15,10 @@ use wasm_bindgen::prelude::*;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ReceivedReceiverWrapper(Arc<AsyncMutex<ReceivedReceiver>>);
|
||||
pub(super) struct ReceivedReceiverWrapper(Arc<AsyncMutex<ReceivedReceiver<WasmTestMessageExt>>>);
|
||||
|
||||
impl ReceivedReceiverWrapper {
|
||||
pub(super) fn new(inner: ReceivedReceiver) -> Self {
|
||||
pub(super) fn new(inner: ReceivedReceiver<WasmTestMessageExt>) -> Self {
|
||||
ReceivedReceiverWrapper(Arc::new(AsyncMutex::new(inner)))
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ impl ReceivedReceiverWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn lock(&self) -> AsyncMutexGuard<'_, ReceivedReceiver> {
|
||||
pub(super) async fn lock(&self) -> AsyncMutexGuard<'_, ReceivedReceiver<WasmTestMessageExt>> {
|
||||
self.0.lock().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl NymNodeTesterBuilder {
|
||||
let tester = NodeTester::new(
|
||||
rng,
|
||||
self.base_topology,
|
||||
address(&self.key_manager, gateway_identity),
|
||||
Some(address(&self.key_manager, gateway_identity)),
|
||||
PacketSize::default(),
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(5),
|
||||
@@ -266,7 +266,12 @@ impl NymNodeTester {
|
||||
let test_ext = WasmTestMessageExt::new(test_nonce);
|
||||
let mut tester_permit = self.tester.lock().expect("mutex got poisoned");
|
||||
tester_permit
|
||||
.existing_identity_mixnode_test_packets(mixnode_identity, test_ext, num_test_packets)
|
||||
.existing_identity_mixnode_test_packets(
|
||||
mixnode_identity,
|
||||
test_ext,
|
||||
num_test_packets,
|
||||
None,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-con
|
||||
nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-name-service-common = { path = "../../cosmwasm-smart-contracts/name-service" }
|
||||
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
|
||||
nym-service-provider-directory-common = { path = "../../cosmwasm-smart-contracts/service-provider-directory" }
|
||||
nym-vesting-contract = { path = "../../../contracts/vesting" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
@@ -63,6 +65,14 @@ name = "offline_signing"
|
||||
# (traits would need to be moved around and refactored themselves)
|
||||
required-features = ["nyxd-client"]
|
||||
|
||||
[[example]]
|
||||
name = "query_service_provider_directory"
|
||||
required-features = ["nyxd-client"]
|
||||
|
||||
[[example]]
|
||||
name = "query_name_service"
|
||||
required-features = ["nyxd-client"]
|
||||
|
||||
[features]
|
||||
nyxd-client = [
|
||||
"async-trait",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use cosmrs::AccountId;
|
||||
use nym_name_service_common::Address;
|
||||
use nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||
use nym_validator_client::nyxd::traits::NameServiceQueryClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap()));
|
||||
let network_details = NymNetworkDetails::new_from_env();
|
||||
let config =
|
||||
nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap();
|
||||
let client = nym_validator_client::Client::new_query(config).unwrap();
|
||||
|
||||
let config = client.nyxd.get_name_service_config().await.unwrap();
|
||||
println!("config: {config:?}");
|
||||
|
||||
let names_paged = client.nyxd.get_names_paged(None, None).await.unwrap();
|
||||
println!("names (paged): {names_paged:#?}");
|
||||
|
||||
let names = client.nyxd.get_all_names().await.unwrap();
|
||||
println!("names: {names:#?}");
|
||||
|
||||
let owner = AccountId::from_str("n1hmf957kc7arcd39rl7xq8l0a4zyg7kxnv7su87").unwrap();
|
||||
let names_by_owner = client.nyxd.get_names_by_owner(owner).await.unwrap();
|
||||
println!("names (by owner): {names_by_owner:#?}");
|
||||
|
||||
let nym_address = Address::new("client_id.client_key@gateway_id");
|
||||
let names_by_address = client.nyxd.get_names_by_address(nym_address).await.unwrap();
|
||||
println!("names (by address): {names_by_address:#?}");
|
||||
|
||||
let service_info = client.nyxd.get_name_entry(1).await;
|
||||
println!("service info: {service_info:#?}");
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use cosmrs::AccountId;
|
||||
use nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||
use nym_service_provider_directory_common::NymAddress;
|
||||
use nym_validator_client::nyxd::traits::SpDirectoryQueryClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap()));
|
||||
let network_details = NymNetworkDetails::new_from_env();
|
||||
let config =
|
||||
nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap();
|
||||
let client = nym_validator_client::Client::new_query(config).unwrap();
|
||||
|
||||
let config = client.nyxd.get_service_config().await.unwrap();
|
||||
println!("config: {config:?}");
|
||||
|
||||
let services_paged = client.nyxd.get_services_paged(None, None).await.unwrap();
|
||||
println!("services (paged): {services_paged:#?}");
|
||||
|
||||
let services = client.nyxd.get_all_services().await.unwrap();
|
||||
println!("services: {services:#?}");
|
||||
|
||||
let announcer = AccountId::from_str("n1hmf957kc7arcd39rl7xq8l0a4zyg7kxnv7su87").unwrap();
|
||||
let services_by_announcer = client
|
||||
.nyxd
|
||||
.get_services_by_announcer(announcer)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("services (by announcer): {services_by_announcer:#?}");
|
||||
|
||||
let nym_address = NymAddress::new("foo.bar@gateway");
|
||||
let services_by_nym_address = client
|
||||
.nyxd
|
||||
.get_services_by_nym_address(nym_address)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(services_by_announcer, services_by_nym_address);
|
||||
|
||||
let service_info = client.nyxd.get_service_info(1).await;
|
||||
println!("service info: {service_info:#?}");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Config as ClientConfig, NyxdClient, QueryNyxdClient};
|
||||
use crate::NymApiClient;
|
||||
use crate::{NymApiClient, ValidatorClientError};
|
||||
|
||||
use crate::nyxd::traits::MixnetQueryClient;
|
||||
use colored::Colorize;
|
||||
@@ -45,6 +45,23 @@ pub async fn run_validator_connection_test<H: BuildHasher + 'static>(
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn test_nyxd_url_connection(
|
||||
network: NymNetworkDetails,
|
||||
nyxd_url: Url,
|
||||
address: cosmrs::AccountId,
|
||||
) -> Result<bool, ValidatorClientError> {
|
||||
let config = ClientConfig::try_from_nym_network_details(&network)
|
||||
.expect("failed to create valid nyxd client config");
|
||||
|
||||
let mut nyxd_client = NyxdClient::<QueryNyxdClient>::connect(config, nyxd_url.as_str())?;
|
||||
// possibly redundant, but lets just leave it here
|
||||
nyxd_client.set_mixnet_contract_address(address);
|
||||
match test_nyxd_connection(network, &nyxd_url, &nyxd_client).await {
|
||||
ConnectionResult::Nyxd(_, _, res) => Ok(res),
|
||||
_ => Ok(false), // ✶ not possible to happens
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_connection_tests<H: BuildHasher + 'static>(
|
||||
nyxd_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
|
||||
api_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
|
||||
@@ -105,7 +122,7 @@ async fn test_nyxd_connection(
|
||||
{
|
||||
Ok(Err(NyxdError::TendermintError(e))) => {
|
||||
// If we get a tendermint-rpc error, we classify the node as not contactable
|
||||
log::debug!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
|
||||
log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
|
||||
false
|
||||
}
|
||||
Ok(Err(NyxdError::AbciError { code, log, .. })) => {
|
||||
@@ -117,13 +134,13 @@ async fn test_nyxd_connection(
|
||||
);
|
||||
code == 18
|
||||
}
|
||||
Ok(Err(error @ NyxdError::NoContractAddressAvailable)) => {
|
||||
log::debug!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
|
||||
Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => {
|
||||
log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
|
||||
false
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// For any other error, we're optimistic and just try anyway.
|
||||
log::debug!(
|
||||
log::warn!(
|
||||
"Checking: nyxd_url: {url}: {}, but with error: {e}",
|
||||
"success".green()
|
||||
);
|
||||
@@ -134,7 +151,7 @@ async fn test_nyxd_connection(
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
|
||||
log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,9 @@ pub enum NymAPIError {
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Request failed with error message - {0}")]
|
||||
GenericRequestFailure(String),
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ use nym_api_requests::models::{
|
||||
};
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
|
||||
use reqwest::Response;
|
||||
use nym_service_provider_directory_common::ServiceInfo;
|
||||
use reqwest::{Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@@ -76,6 +77,8 @@ impl Client {
|
||||
let res = self.send_get_request(path, params).await?;
|
||||
if res.status().is_success() {
|
||||
Ok(res.json().await?)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(NymAPIError::NotFound)
|
||||
} else {
|
||||
Err(NymAPIError::GenericRequestFailure(res.text().await?))
|
||||
}
|
||||
@@ -166,7 +169,7 @@ impl Client {
|
||||
|
||||
pub async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.query_nym_api(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||
&[routes::API_VERSION, routes::MIXNODES],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -480,6 +483,11 @@ impl Client {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_service_providers(&self) -> Result<Vec<ServiceInfo>, NymAPIError> {
|
||||
self.query_nym_api(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// utility function that should solve the double slash problem in validator API forever.
|
||||
|
||||
@@ -32,3 +32,5 @@ pub const COMPUTE_REWARD_ESTIMATION: &str = "compute-reward-estimation";
|
||||
pub const AVG_UPTIME: &str = "avg_uptime";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
|
||||
|
||||
pub const SERVICE_PROVIDERS: &str = "service-providers";
|
||||
|
||||
@@ -21,8 +21,8 @@ use std::{io, time::Duration};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NyxdError {
|
||||
#[error("No contract address is available to perform the call")]
|
||||
NoContractAddressAvailable,
|
||||
#[error("No contract address is available to perform the call: {0}")]
|
||||
NoContractAddressAvailable(String),
|
||||
|
||||
#[error(transparent)]
|
||||
WalletError(#[from] DirectSecp256k1HdWalletError),
|
||||
@@ -162,7 +162,7 @@ fn try_parse_abci_log(log: &abci::Log) -> Option<String> {
|
||||
.value()
|
||||
.contains("Maximum amount of locked coins has already been pledged")
|
||||
{
|
||||
Some("Maximum amount of locked tokens has alredy been used. You can only use up to 10% of your locked tokens for bonding and delegating.".to_string())
|
||||
Some("Maximum amount of locked tokens has already been used. You can only use up to 10% of your locked tokens for bonding and delegating.".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ pub struct Config {
|
||||
pub(crate) group_contract_address: Option<AccountId>,
|
||||
pub(crate) multisig_contract_address: Option<AccountId>,
|
||||
pub(crate) coconut_dkg_contract_address: Option<AccountId>,
|
||||
pub(crate) service_provider_contract_address: Option<AccountId>,
|
||||
pub(crate) name_service_contract_address: Option<AccountId>,
|
||||
// TODO: add this in later commits
|
||||
// pub(crate) gas_price: GasPrice,
|
||||
}
|
||||
@@ -131,6 +133,17 @@ impl Config {
|
||||
details.contracts.coconut_dkg_contract_address.as_ref(),
|
||||
prefix,
|
||||
)?,
|
||||
service_provider_contract_address: Self::parse_optional_account(
|
||||
details
|
||||
.contracts
|
||||
.service_provider_directory_contract_address
|
||||
.as_ref(),
|
||||
prefix,
|
||||
)?,
|
||||
name_service_contract_address: Self::parse_optional_account(
|
||||
details.contracts.name_service_contract_address.as_ref(),
|
||||
prefix,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -246,6 +259,10 @@ impl<C> NyxdClient<C> {
|
||||
self.config.multisig_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_service_provider_contract_address(&mut self, address: AccountId) {
|
||||
self.config.service_provider_contract_address = Some(address);
|
||||
}
|
||||
|
||||
// TODO: this should get changed into Result<&AccountId, NyxdError> (or Option<&AccountId> in future commits
|
||||
// note: what unwrap is doing here is just moving a failure that would have normally
|
||||
// occurred in `connect` when attempting to parse an empty address,
|
||||
@@ -304,6 +321,16 @@ impl<C> NyxdClient<C> {
|
||||
self.config.coconut_dkg_contract_address.as_ref().unwrap()
|
||||
}
|
||||
|
||||
// The service provider directory contract is optional, so we return an Option not a Result
|
||||
pub fn service_provider_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.service_provider_contract_address.as_ref()
|
||||
}
|
||||
|
||||
// The name service contract is optional, so we return an Option not a Result
|
||||
pub fn name_service_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.name_service_contract_address.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,25 @@ mod mixnet_signing_client;
|
||||
mod multisig_signing_client;
|
||||
mod vesting_signing_client;
|
||||
|
||||
mod sp_directory_query_client;
|
||||
mod sp_directory_signing_client;
|
||||
|
||||
mod name_service_query_client;
|
||||
mod name_service_signing_client;
|
||||
|
||||
pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient;
|
||||
pub use dkg_query_client::DkgQueryClient;
|
||||
pub use group_query_client::GroupQueryClient;
|
||||
pub use mixnet_query_client::MixnetQueryClient;
|
||||
pub use multisig_query_client::MultisigQueryClient;
|
||||
pub use name_service_query_client::NameServiceQueryClient;
|
||||
pub use sp_directory_query_client::SpDirectoryQueryClient;
|
||||
pub use vesting_query_client::VestingQueryClient;
|
||||
|
||||
pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient;
|
||||
pub use dkg_signing_client::DkgSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use name_service_signing_client::NameServiceSigningClient;
|
||||
pub use sp_directory_signing_client::SpDirectorySigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_name_service_common::{
|
||||
msg::QueryMsg as NameQueryMsg,
|
||||
response::{ConfigResponse, NamesListResponse, PagedNamesListResponse},
|
||||
Address, NameEntry, NameId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::nyxd::{error::NyxdError, CosmWasmClient, NyxdClient};
|
||||
|
||||
#[async_trait]
|
||||
pub trait NameServiceQueryClient {
|
||||
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_name_service_config(&self) -> Result<ConfigResponse, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::Config {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_name_entry(&self, name_id: NameId) -> Result<NameEntry, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::NameId { name_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_names_paged(
|
||||
&self,
|
||||
start_after: Option<NameId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedNamesListResponse, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::All { limit, start_after })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_names_by_owner(&self, owner: AccountId) -> Result<NamesListResponse, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::ByOwner {
|
||||
owner: owner.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_names_by_address(&self, address: Address) -> Result<NamesListResponse, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::ByAddress { address })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_name_service_contract_version(
|
||||
&self,
|
||||
) -> Result<ContractBuildInformation, NyxdError> {
|
||||
self.query_name_service_contract(NameQueryMsg::GetContractVersion {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_names(&self) -> Result<Vec<NameEntry>, NyxdError> {
|
||||
let mut services = Vec::new();
|
||||
let mut start_after = None;
|
||||
|
||||
loop {
|
||||
let mut paged_response = self.get_names_paged(start_after.take(), None).await?;
|
||||
|
||||
let last_id = paged_response.names.last().map(|serv| serv.name_id);
|
||||
services.append(&mut paged_response.names);
|
||||
|
||||
if let Some(start_after_res) = last_id {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> NameServiceQueryClient for NyxdClient<C>
|
||||
where
|
||||
C: CosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.client
|
||||
.query_contract_smart(
|
||||
self.name_service_contract_address().ok_or(
|
||||
NyxdError::NoContractAddressAvailable("name service contract".to_string()),
|
||||
)?,
|
||||
&query,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> NameServiceQueryClient for crate::Client<C>
|
||||
where
|
||||
C: CosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.nyxd.query_name_service_contract(query).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_name_service_common::{msg::ExecuteMsg as NameExecuteMsg, Address, NameId, NymName};
|
||||
|
||||
use crate::nyxd::{
|
||||
coin::Coin, cosmwasm_client::types::ExecuteResult, error::NyxdError, Fee, NyxdClient,
|
||||
SigningCosmWasmClient,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait NameServiceSigningClient {
|
||||
async fn execute_name_service_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NameExecuteMsg,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn register_name(
|
||||
&self,
|
||||
name: NymName,
|
||||
address: Address,
|
||||
deposit: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_name_service_contract(
|
||||
fee,
|
||||
NameExecuteMsg::Register { name, address },
|
||||
vec![deposit],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_name_by_id(
|
||||
&self,
|
||||
name_id: NameId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_name_service_contract(fee, NameExecuteMsg::DeleteId { name_id }, vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_service_provider_by_name(
|
||||
&self,
|
||||
name: NymName,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_name_service_contract(fee, NameExecuteMsg::DeleteName { name }, vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_deposit_required(
|
||||
&self,
|
||||
deposit_required: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_name_service_contract(
|
||||
fee,
|
||||
NameExecuteMsg::UpdateDepositRequired {
|
||||
deposit_required: deposit_required.into(),
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> NameServiceSigningClient for NyxdClient<C>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync + Send,
|
||||
{
|
||||
async fn execute_name_service_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NameExecuteMsg,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
|
||||
let memo = msg.default_memo();
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.name_service_contract_address().ok_or(
|
||||
NyxdError::NoContractAddressAvailable("name service contract".to_string()),
|
||||
)?,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_service_provider_directory_common::{
|
||||
msg::QueryMsg as SpQueryMsg,
|
||||
response::{
|
||||
ConfigResponse, PagedServicesListResponse, ServiceInfoResponse, ServicesListResponse,
|
||||
},
|
||||
NymAddress, ServiceId, ServiceInfo,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::nyxd::{error::NyxdError, CosmWasmClient, NyxdClient};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SpDirectoryQueryClient {
|
||||
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_service_config(&self) -> Result<ConfigResponse, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::Config {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_service_info(
|
||||
&self,
|
||||
service_id: ServiceId,
|
||||
) -> Result<ServiceInfoResponse, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::ServiceId { service_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_services_paged(
|
||||
&self,
|
||||
start_after: Option<ServiceId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedServicesListResponse, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::All { limit, start_after })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_services_by_announcer(
|
||||
&self,
|
||||
announcer: AccountId,
|
||||
) -> Result<ServicesListResponse, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::ByAnnouncer {
|
||||
announcer: announcer.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_services_by_nym_address(
|
||||
&self,
|
||||
nym_address: NymAddress,
|
||||
) -> Result<ServicesListResponse, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::ByNymAddress { nym_address })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_sp_contract_version(&self) -> Result<ContractBuildInformation, NyxdError> {
|
||||
self.query_service_provider_contract(SpQueryMsg::GetContractVersion {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_services(&self) -> Result<Vec<ServiceInfo>, NyxdError> {
|
||||
let mut services = Vec::new();
|
||||
let mut start_after = None;
|
||||
|
||||
loop {
|
||||
let mut paged_response = self.get_services_paged(start_after.take(), None).await?;
|
||||
|
||||
let last_id = paged_response.services.last().map(|serv| serv.service_id);
|
||||
services.append(&mut paged_response.services);
|
||||
|
||||
if let Some(start_after_res) = last_id {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> SpDirectoryQueryClient for NyxdClient<C>
|
||||
where
|
||||
C: CosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.client
|
||||
.query_contract_smart(
|
||||
self.service_provider_contract_address().ok_or(
|
||||
NyxdError::NoContractAddressAvailable(
|
||||
"service provider directory contract".to_string(),
|
||||
),
|
||||
)?,
|
||||
&query,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> SpDirectoryQueryClient for crate::Client<C>
|
||||
where
|
||||
C: CosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.nyxd.query_service_provider_contract(query).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_service_provider_directory_common::{
|
||||
msg::ExecuteMsg as SpExecuteMsg, NymAddress, ServiceId, ServiceType,
|
||||
};
|
||||
|
||||
use crate::nyxd::{
|
||||
coin::Coin, cosmwasm_client::types::ExecuteResult, error::NyxdError, Fee, NyxdClient,
|
||||
SigningCosmWasmClient,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SpDirectorySigningClient {
|
||||
async fn execute_service_provider_directory_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: SpExecuteMsg,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn announce_service_provider(
|
||||
&self,
|
||||
nym_address: NymAddress,
|
||||
service_type: ServiceType,
|
||||
deposit: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_service_provider_directory_contract(
|
||||
fee,
|
||||
SpExecuteMsg::Announce {
|
||||
nym_address,
|
||||
service_type,
|
||||
},
|
||||
vec![deposit],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_service_provider_by_id(
|
||||
&self,
|
||||
service_id: ServiceId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_service_provider_directory_contract(
|
||||
fee,
|
||||
SpExecuteMsg::DeleteId { service_id },
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_service_provider_by_nym_address(
|
||||
&self,
|
||||
nym_address: NymAddress,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_service_provider_directory_contract(
|
||||
fee,
|
||||
SpExecuteMsg::DeleteNymAddress { nym_address },
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_deposit_required(
|
||||
&self,
|
||||
deposit_required: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_service_provider_directory_contract(
|
||||
fee,
|
||||
SpExecuteMsg::UpdateDepositRequired {
|
||||
deposit_required: deposit_required.into(),
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> SpDirectorySigningClient for NyxdClient<C>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync + Send,
|
||||
{
|
||||
async fn execute_service_provider_directory_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: SpExecuteMsg,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
|
||||
let memo = msg.default_memo();
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.service_provider_contract_address().ok_or(
|
||||
NyxdError::NoContractAddressAvailable(
|
||||
"service provider directory contract".to_string(),
|
||||
),
|
||||
)?,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -38,3 +38,4 @@ nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-cont
|
||||
nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-service-provider-directory-common = { path = "../cosmwasm-smart-contracts/service-provider-directory" }
|
||||
|
||||
@@ -5,6 +5,7 @@ use clap::{Args, Subcommand};
|
||||
|
||||
pub mod gateway;
|
||||
pub mod mixnode;
|
||||
pub mod service;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||
@@ -19,4 +20,6 @@ pub enum MixnetOperatorsCommands {
|
||||
Mixnode(mixnode::MixnetOperatorsMixnode),
|
||||
/// Manage your gateway
|
||||
Gateway(gateway::MixnetOperatorsGateway),
|
||||
/// Manage your service
|
||||
ServiceProvider(service::MixnetOperatorsService),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use nym_service_provider_directory_common::{Coin, NymAddress, ServiceType};
|
||||
use nym_validator_client::nyxd::traits::SpDirectorySigningClient;
|
||||
|
||||
use crate::context::SigningClient;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[clap(long)]
|
||||
pub nym_address: String,
|
||||
|
||||
/// Deposit to be made to the service provider directory, in curent DENOMINATION (e.g. 'unym')
|
||||
#[clap(long)]
|
||||
pub deposit: u128,
|
||||
}
|
||||
|
||||
pub async fn announce(args: Args, client: SigningClient) {
|
||||
info!("Annoucing service provider");
|
||||
|
||||
let nym_address = NymAddress::Address(args.nym_address);
|
||||
let service_type = ServiceType::NetworkRequester;
|
||||
|
||||
let denom = client.current_chain_details().mix_denom.base.as_str();
|
||||
let deposit = Coin::new(args.deposit, denom);
|
||||
|
||||
let res = client
|
||||
.announce_service_provider(nym_address, service_type, deposit.into(), None)
|
||||
.await
|
||||
.expect("Failed to announce service provider");
|
||||
|
||||
info!("Announced service provider: {res:?}");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use nym_service_provider_directory_common::ServiceId;
|
||||
use nym_validator_client::nyxd::traits::SpDirectorySigningClient;
|
||||
|
||||
use crate::context::SigningClient;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[clap(long)]
|
||||
pub id: ServiceId,
|
||||
}
|
||||
|
||||
pub async fn delete(args: Args, client: SigningClient) {
|
||||
info!("Deleting service provider with id {}", args.id);
|
||||
|
||||
let res = client
|
||||
.delete_service_provider_by_id(args.id, None)
|
||||
.await
|
||||
.expect("Failed to delete service provider");
|
||||
|
||||
info!("Deleted: {res:?}");
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use clap::{Args, Subcommand};
|
||||
|
||||
pub mod announce;
|
||||
pub mod delete;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||
pub struct MixnetOperatorsService {
|
||||
#[clap(subcommand)]
|
||||
pub command: MixnetOperatorsServiceCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum MixnetOperatorsServiceCommands {
|
||||
/// Announce service provider to the world
|
||||
Announce(announce::Args),
|
||||
/// Delete entry for service provider from the directory
|
||||
Delete(delete::Args),
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use clap::{Args, Subcommand};
|
||||
|
||||
pub mod query_all_gateways;
|
||||
pub mod query_all_mixnodes;
|
||||
pub mod query_all_service_providers;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||
@@ -19,4 +20,6 @@ pub enum MixnetQueryCommands {
|
||||
Mixnodes(query_all_mixnodes::Args),
|
||||
/// Query gateways
|
||||
Gateways(query_all_gateways::Args),
|
||||
/// Query announced service-providers
|
||||
ServiceProviders(query_all_service_providers::Args),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use comfy_table::Table;
|
||||
use nym_validator_client::nym_api::error::NymAPIError;
|
||||
|
||||
use crate::context::QueryClientWithNyxd;
|
||||
use crate::utils::show_error;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[clap(value_parser)]
|
||||
#[clap(help = "Optionally, the service provider to display")]
|
||||
pub nym_address: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
||||
match client.nym_api.get_service_providers().await {
|
||||
Ok(res) => {
|
||||
if let Some(nym_address) = args.nym_address {
|
||||
let service = res.iter().find(|service| {
|
||||
service
|
||||
.service
|
||||
.nym_address
|
||||
.to_string()
|
||||
.eq_ignore_ascii_case(&nym_address)
|
||||
});
|
||||
println!(
|
||||
"{}",
|
||||
::serde_json::to_string_pretty(&service).expect("json formatting error")
|
||||
);
|
||||
} else {
|
||||
let mut table = Table::new();
|
||||
|
||||
table.set_header(vec!["Service Id", "Announcer", "Nym Address"]);
|
||||
for service in res {
|
||||
table.add_row(vec![
|
||||
service.service_id.to_string(),
|
||||
service.service.announcer.to_string(),
|
||||
service.service.service_type.to_string(),
|
||||
service.service.nym_address.to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
println!("The service providers in the directory are:");
|
||||
println!("{table}");
|
||||
}
|
||||
}
|
||||
Err(NymAPIError::NotFound) => {
|
||||
println!("nym-api reports no service provider endpoint available");
|
||||
}
|
||||
Err(e) => show_error(e),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "nym-name-service-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = "0.8"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -0,0 +1,66 @@
|
||||
use cosmwasm_std::{Coin, Event};
|
||||
|
||||
use crate::{NameId, RegisteredName};
|
||||
|
||||
pub enum NameEventType {
|
||||
Register,
|
||||
DeleteId,
|
||||
DeleteName,
|
||||
UpdateDepositRequired,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NameEventType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NameEventType::Register => write!(f, "register"),
|
||||
NameEventType::DeleteId => write!(f, "delete_id"),
|
||||
NameEventType::DeleteName => write!(f, "delete_name"),
|
||||
NameEventType::UpdateDepositRequired => write!(f, "update_deposit_required"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NameEventType> for String {
|
||||
fn from(event_type: NameEventType) -> Self {
|
||||
event_type.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub const ACTION: &str = "action";
|
||||
|
||||
pub const NAME_ID: &str = "name_id";
|
||||
pub const NAME: &str = "name";
|
||||
pub const OWNER: &str = "owner";
|
||||
|
||||
pub const DEPOSIT_REQUIRED: &str = "deposit_required";
|
||||
|
||||
pub fn new_register_event(name_id: NameId, name: RegisteredName) -> Event {
|
||||
Event::new(NameEventType::Register)
|
||||
.add_attribute(ACTION, NameEventType::Register)
|
||||
.add_attribute(NAME_ID, name_id.to_string())
|
||||
.add_attribute(NAME, name.name.to_string())
|
||||
.add_attribute(name.address.event_tag(), name.address.to_string())
|
||||
.add_attribute(OWNER, name.owner.to_string())
|
||||
}
|
||||
|
||||
pub fn new_delete_id_event(name_id: NameId, name: RegisteredName) -> Event {
|
||||
Event::new(NameEventType::DeleteId)
|
||||
.add_attribute(ACTION, NameEventType::DeleteId)
|
||||
.add_attribute(NAME_ID, name_id.to_string())
|
||||
.add_attribute(NAME, name.name.to_string())
|
||||
.add_attribute(name.address.event_tag(), name.address.to_string())
|
||||
}
|
||||
|
||||
pub fn new_delete_name_event(name_id: NameId, name: RegisteredName) -> Event {
|
||||
Event::new(NameEventType::DeleteId)
|
||||
.add_attribute(ACTION, NameEventType::DeleteName)
|
||||
.add_attribute(NAME_ID, name_id.to_string())
|
||||
.add_attribute(NAME, name.name.to_string())
|
||||
.add_attribute(name.address.event_tag(), name.address.to_string())
|
||||
}
|
||||
|
||||
pub fn new_update_deposit_required_event(deposit_required: Coin) -> Event {
|
||||
Event::new(NameEventType::UpdateDepositRequired)
|
||||
.add_attribute(ACTION, NameEventType::UpdateDepositRequired)
|
||||
.add_attribute(DEPOSIT_REQUIRED, deposit_required.to_string())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod events;
|
||||
pub mod msg;
|
||||
pub mod response;
|
||||
pub mod types;
|
||||
|
||||
// Re-export all types at the top-level
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,91 @@
|
||||
use crate::{Address, NameId, NymName};
|
||||
use cosmwasm_std::Coin;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct InstantiateMsg {
|
||||
pub deposit_required: Coin,
|
||||
}
|
||||
|
||||
impl InstantiateMsg {
|
||||
pub fn new(deposit_required: Coin) -> Self {
|
||||
Self { deposit_required }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
/// Announcing a name pointing to a nym-address
|
||||
Register { name: NymName, address: Address },
|
||||
/// Delete a name entry by id
|
||||
DeleteId { name_id: NameId },
|
||||
/// Delete a name entry by name
|
||||
DeleteName { name: NymName },
|
||||
/// Change the deposit required for announcing a name
|
||||
UpdateDepositRequired { deposit_required: Coin },
|
||||
}
|
||||
|
||||
impl ExecuteMsg {
|
||||
pub fn delete_id(name_id: NameId) -> Self {
|
||||
ExecuteMsg::DeleteId { name_id }
|
||||
}
|
||||
|
||||
pub fn default_memo(&self) -> String {
|
||||
match self {
|
||||
ExecuteMsg::Register { name, address } => {
|
||||
format!("registering {address} as name: {name}")
|
||||
}
|
||||
ExecuteMsg::DeleteId { name_id } => {
|
||||
format!("deleting name with id {name_id}")
|
||||
}
|
||||
ExecuteMsg::DeleteName { name } => {
|
||||
format!("deleting name: {name}")
|
||||
}
|
||||
ExecuteMsg::UpdateDepositRequired { deposit_required } => {
|
||||
format!("updating the deposit required to {deposit_required}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
/// Query the name by it's assigned id
|
||||
NameId {
|
||||
name_id: NameId,
|
||||
},
|
||||
// Query the names by the registrator
|
||||
ByOwner {
|
||||
owner: String,
|
||||
},
|
||||
ByName {
|
||||
name: NymName,
|
||||
},
|
||||
ByAddress {
|
||||
address: Address,
|
||||
},
|
||||
All {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<NameId>,
|
||||
},
|
||||
Config {},
|
||||
GetContractVersion {},
|
||||
#[serde(rename = "get_cw2_contract_version")]
|
||||
GetCW2ContractVersion {},
|
||||
}
|
||||
|
||||
impl QueryMsg {
|
||||
pub fn all() -> QueryMsg {
|
||||
QueryMsg::All {
|
||||
limit: None,
|
||||
start_after: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use crate::{msg::ExecuteMsg, NameEntry, NameId, RegisteredName};
|
||||
use cosmwasm_std::Coin;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Like [`NameEntry`] but since it's a response type the name is an option depending on if
|
||||
/// the name exists or not.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct NameEntryResponse {
|
||||
pub name_id: NameId,
|
||||
pub name: Option<RegisteredName>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct NamesListResponse {
|
||||
pub names: Vec<NameEntry>,
|
||||
}
|
||||
|
||||
impl NamesListResponse {
|
||||
pub fn new(names: Vec<(NameId, RegisteredName)>) -> NamesListResponse {
|
||||
NamesListResponse {
|
||||
names: names
|
||||
.into_iter()
|
||||
.map(|(name_id, name)| NameEntry::new(name_id, name))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[NameEntry]> for NamesListResponse {
|
||||
fn from(names: &[NameEntry]) -> Self {
|
||||
NamesListResponse {
|
||||
names: names.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct PagedNamesListResponse {
|
||||
pub names: Vec<NameEntry>,
|
||||
pub per_page: usize,
|
||||
pub start_next_after: Option<NameId>,
|
||||
}
|
||||
|
||||
impl PagedNamesListResponse {
|
||||
pub fn new(
|
||||
names: Vec<(NameId, RegisteredName)>,
|
||||
per_page: usize,
|
||||
start_next_after: Option<NameId>,
|
||||
) -> PagedNamesListResponse {
|
||||
let names = names
|
||||
.into_iter()
|
||||
.map(|(name_id, name)| NameEntry::new(name_id, name))
|
||||
.collect();
|
||||
PagedNamesListResponse {
|
||||
names,
|
||||
per_page,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ConfigResponse {
|
||||
pub deposit_required: Coin,
|
||||
}
|
||||
|
||||
impl From<RegisteredName> for ExecuteMsg {
|
||||
fn from(name: RegisteredName) -> Self {
|
||||
ExecuteMsg::Register {
|
||||
name: name.name,
|
||||
address: name.address,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The directory of services are indexed by [`ServiceId`].
|
||||
pub type NameId = u32;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)]
|
||||
pub struct RegisteredName {
|
||||
/// The name pointing to the nym address
|
||||
pub name: NymName,
|
||||
/// The address of the service.
|
||||
pub address: Address,
|
||||
/// Service owner.
|
||||
pub owner: Addr,
|
||||
/// Block height at which the service was added.
|
||||
pub block_height: u64,
|
||||
/// The deposit used to announce the service.
|
||||
pub deposit: Coin,
|
||||
}
|
||||
|
||||
/// String representation of a nym address, which is of the form
|
||||
/// client_id.client_enc@gateway_id.
|
||||
/// NOTE: entirely unvalidated.
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Address {
|
||||
NymAddress(String),
|
||||
// Possible extension:
|
||||
//Gateway(String)
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Create a new nym address.
|
||||
pub fn new(address: &str) -> Self {
|
||||
Self::NymAddress(address.to_string())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Address::NymAddress(address) => address,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_tag(&self) -> &str {
|
||||
match self {
|
||||
Address::NymAddress(_) => "nym_address",
|
||||
//Address::Gateway(_) => "gatway_address",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Name stored and pointing a to a nym-address
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct NymName(String);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NymNameError {
|
||||
InvalidName,
|
||||
}
|
||||
|
||||
fn is_valid_name_char(c: char) -> bool {
|
||||
// Normal lowercase letters
|
||||
(c.is_alphabetic() && c.is_lowercase())
|
||||
// or numbers
|
||||
|| c.is_numeric()
|
||||
// special case hyphen or underscore
|
||||
|| c == '-' || c == '_'
|
||||
}
|
||||
|
||||
impl NymName {
|
||||
pub fn new(name: &str) -> Result<NymName, NymNameError> {
|
||||
// We are a bit restrictive in which names we allow, to start out with. Consider relaxing
|
||||
// this in the future.
|
||||
if !name.chars().all(is_valid_name_char) {
|
||||
return Err(NymNameError::InvalidName);
|
||||
}
|
||||
Ok(Self(name.to_string()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NymName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`RegisterdName`] together with the assigned [`NameId`].
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct NameEntry {
|
||||
pub name_id: NameId,
|
||||
pub name: RegisteredName,
|
||||
}
|
||||
|
||||
impl NameEntry {
|
||||
pub fn new(name_id: NameId, name: RegisteredName) -> Self {
|
||||
Self { name_id, name }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NymName;
|
||||
|
||||
#[test]
|
||||
fn parse_nym_name() {
|
||||
// Test some valid cases
|
||||
assert!(NymName::new("foo").is_ok());
|
||||
assert!(NymName::new("foo-bar").is_ok());
|
||||
assert!(NymName::new("foo-bar-123").is_ok());
|
||||
assert!(NymName::new("foo_bar").is_ok());
|
||||
assert!(NymName::new("foo_bar_123").is_ok());
|
||||
|
||||
// And now test all some invalid ones
|
||||
assert!(NymName::new("Foo").is_err());
|
||||
assert!(NymName::new("foo bar").is_err());
|
||||
assert!(NymName::new("foo!bar").is_err());
|
||||
assert!(NymName::new("foo#bar").is_err());
|
||||
assert!(NymName::new("foo$bar").is_err());
|
||||
assert!(NymName::new("foo%bar").is_err());
|
||||
assert!(NymName::new("foo&bar").is_err());
|
||||
assert!(NymName::new("foo'bar").is_err());
|
||||
assert!(NymName::new("foo(bar").is_err());
|
||||
assert!(NymName::new("foo)bar").is_err());
|
||||
assert!(NymName::new("foo*bar").is_err());
|
||||
assert!(NymName::new("foo+bar").is_err());
|
||||
assert!(NymName::new("foo,bar").is_err());
|
||||
assert!(NymName::new("foo.bar").is_err());
|
||||
assert!(NymName::new("foo.bar").is_err());
|
||||
assert!(NymName::new("foo/bar").is_err());
|
||||
assert!(NymName::new("foo/bar").is_err());
|
||||
assert!(NymName::new("foo:bar").is_err());
|
||||
assert!(NymName::new("foo;bar").is_err());
|
||||
assert!(NymName::new("foo<bar").is_err());
|
||||
assert!(NymName::new("foo=bar").is_err());
|
||||
assert!(NymName::new("foo>bar").is_err());
|
||||
assert!(NymName::new("foo?bar").is_err());
|
||||
assert!(NymName::new("foo@bar").is_err());
|
||||
assert!(NymName::new("fooBar").is_err());
|
||||
assert!(NymName::new("foo[bar").is_err());
|
||||
assert!(NymName::new("foo\"bar").is_err());
|
||||
assert!(NymName::new("foo\\bar").is_err());
|
||||
assert!(NymName::new("foo]bar").is_err());
|
||||
assert!(NymName::new("foo^bar").is_err());
|
||||
assert!(NymName::new("foo`bar").is_err());
|
||||
assert!(NymName::new("foo{bar").is_err());
|
||||
assert!(NymName::new("foo|bar").is_err());
|
||||
assert!(NymName::new("foo}bar").is_err());
|
||||
assert!(NymName::new("foo~bar").is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -5,3 +5,5 @@ pub mod types;
|
||||
|
||||
// Re-export all types at the top-level
|
||||
pub use types::*;
|
||||
|
||||
pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction};
|
||||
|
||||
@@ -40,6 +40,24 @@ impl ExecuteMsg {
|
||||
pub fn delete_id(service_id: ServiceId) -> Self {
|
||||
ExecuteMsg::DeleteId { service_id }
|
||||
}
|
||||
|
||||
pub fn default_memo(&self) -> String {
|
||||
match self {
|
||||
ExecuteMsg::Announce {
|
||||
nym_address,
|
||||
service_type,
|
||||
} => format!("announcing {nym_address} as type {service_type}"),
|
||||
ExecuteMsg::DeleteId { service_id } => {
|
||||
format!("deleting service with service id {service_id}")
|
||||
}
|
||||
ExecuteMsg::DeleteNymAddress { nym_address } => {
|
||||
format!("deleting service with nym address {nym_address}")
|
||||
}
|
||||
ExecuteMsg::UpdateDepositRequired { deposit_required } => {
|
||||
format!("updating the deposit required to {deposit_required}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{msg::ExecuteMsg, Service, ServiceId, ServiceInfo};
|
||||
use cosmwasm_std::Coin;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
@@ -9,7 +10,7 @@ pub struct ServiceInfoResponse {
|
||||
pub service: Option<Service>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ServicesListResponse {
|
||||
pub services: Vec<ServiceInfo>,
|
||||
@@ -26,6 +27,14 @@ impl ServicesListResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[ServiceInfo]> for ServicesListResponse {
|
||||
fn from(services: &[ServiceInfo]) -> Self {
|
||||
Self {
|
||||
services: services.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct PagedServicesListResponse {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The directory of services are indexed by [`ServiceId`].
|
||||
pub type ServiceId = u32;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)]
|
||||
pub struct Service {
|
||||
/// The address of the service.
|
||||
pub nym_address: NymAddress,
|
||||
@@ -21,7 +22,7 @@ pub struct Service {
|
||||
}
|
||||
|
||||
/// The types of addresses supported.
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NymAddress {
|
||||
/// String representation of a nym address, which is of the form
|
||||
@@ -51,7 +52,7 @@ impl Display for NymAddress {
|
||||
}
|
||||
|
||||
/// The type of services provider supported
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServiceType {
|
||||
NetworkRequester,
|
||||
@@ -66,7 +67,7 @@ impl std::fmt::Display for ServiceType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ServiceInfo {
|
||||
pub service_id: ServiceId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-crypto"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "Crypto library for the nym mixnet"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -24,6 +24,7 @@ serde_bytes = { version = "0.11.6", optional = true }
|
||||
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
|
||||
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
thiserror = "1.0.37"
|
||||
zeroize = { version = "1.5.7", optional = true, features = ["zeroize_derive"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
use nym_sphinx_types::{
|
||||
SharedSecret, RoutingKeys
|
||||
};
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyStorage(Arc<Mutex<HashMap<SharedSecret, (RoutingKeys, Option<SharedSecret>)>>>);
|
||||
|
||||
impl KeyStorage {
|
||||
pub fn new() -> Self {
|
||||
KeyStorage(Arc::new(Mutex::new(
|
||||
HashMap::new()
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
pub fn lookup(&self, key : SharedSecret) -> Option<(RoutingKeys, Option<SharedSecret>)> {
|
||||
match self.0.lock() {
|
||||
Ok(map) => map.get(&key).cloned(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(&self, key : SharedSecret, routing_key : RoutingKeys, blinded_shared_secret : Option<SharedSecret>) {
|
||||
match self.0.lock() {
|
||||
Ok(mut map) => map.insert(key, (routing_key, blinded_shared_secret)),
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
pub mod error;
|
||||
pub mod processor;
|
||||
mod key_storage;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::measure;
|
||||
use crate::packet_processor::error::MixProcessingError;
|
||||
use crate::packet_processor::key_storage::KeyStorage;
|
||||
use log::*;
|
||||
use nym_sphinx_acknowledgements::surb_ack::SurbAck;
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
@@ -11,7 +12,7 @@ use nym_sphinx_framing::packet::FramedSphinxPacket;
|
||||
use nym_sphinx_params::{PacketMode, PacketSize};
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, Payload, PrivateKey,
|
||||
ProcessedPacket, SphinxPacket,
|
||||
ProcessedPacket, RoutingKeys, SharedSecret, SphinxPacket,
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
@@ -38,6 +39,7 @@ pub enum MixProcessingResult {
|
||||
pub struct SphinxPacketProcessor {
|
||||
/// Private sphinx key of this node required to unwrap received sphinx packet.
|
||||
sphinx_key: Arc<PrivateKey>,
|
||||
key_storage: KeyStorage,
|
||||
}
|
||||
|
||||
impl SphinxPacketProcessor {
|
||||
@@ -45,6 +47,7 @@ impl SphinxPacketProcessor {
|
||||
pub fn new(sphinx_key: PrivateKey) -> Self {
|
||||
SphinxPacketProcessor {
|
||||
sphinx_key: Arc::new(sphinx_key),
|
||||
key_storage: KeyStorage::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +68,27 @@ impl SphinxPacketProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
/// Performs a sphinx unwrapping using given keys.
|
||||
#[cfg_attr(
|
||||
feature = "cpucycles",
|
||||
instrument(skip(self, packet), fields(cpucycles))
|
||||
)]
|
||||
fn perform_initial_sphinx_packet_processing_with_keys(
|
||||
&self,
|
||||
packet: SphinxPacket,
|
||||
new_blinded_secret: &Option<SharedSecret>,
|
||||
routing_keys: RoutingKeys,
|
||||
) -> Result<ProcessedPacket, MixProcessingError> {
|
||||
measure!({
|
||||
packet
|
||||
.process_with_derived_keys(new_blinded_secret, routing_keys)
|
||||
.map_err(|err| {
|
||||
debug!("Failed to unwrap Sphinx packet: {err}");
|
||||
MixProcessingError::SphinxProcessingError(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Takes the received framed packet and tries to unwrap it from the sphinx encryption.
|
||||
#[cfg_attr(
|
||||
feature = "cpucycles",
|
||||
@@ -81,8 +105,28 @@ impl SphinxPacketProcessor {
|
||||
if packet_mode.is_old_vpn() {
|
||||
return Err(MixProcessingError::ReceivedOldTypeVpnPacket);
|
||||
}
|
||||
|
||||
self.perform_initial_sphinx_packet_processing(sphinx_packet)
|
||||
//here be shared secret retrieval and hashmap lookup
|
||||
if let Some((routing_keys, blinded_shared_secret)) =
|
||||
self.key_storage.lookup(sphinx_packet.shared_secret())
|
||||
{
|
||||
trace!("Packet already seen, reusing keys");
|
||||
self.perform_initial_sphinx_packet_processing_with_keys(
|
||||
sphinx_packet,
|
||||
&blinded_shared_secret,
|
||||
routing_keys,
|
||||
)
|
||||
} else {
|
||||
trace!("New packet, deriving keys and storing them");
|
||||
let key_secret = sphinx_packet.shared_secret();
|
||||
let processed_packet =
|
||||
self.perform_initial_sphinx_packet_processing(sphinx_packet)?;
|
||||
self.key_storage.store(
|
||||
key_secret,
|
||||
processed_packet.routing_keys(),
|
||||
processed_packet.shared_secret(),
|
||||
);
|
||||
Ok(processed_packet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,12 +219,12 @@ impl SphinxPacketProcessor {
|
||||
packet_mode: PacketMode,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
match packet {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay) => {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay, _) => {
|
||||
self.process_forward_hop(*packet, address, delay, packet_mode)
|
||||
}
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacket::FinalHop(destination, _, payload) => {
|
||||
ProcessedPacket::FinalHop(destination, _, payload, _) => {
|
||||
self.process_final_hop(destination, payload, packet_size, packet_mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
|
||||
use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env::var, ops::Not, path::PathBuf};
|
||||
use std::{
|
||||
env::{var, VarError},
|
||||
ffi::OsStr,
|
||||
ops::Not,
|
||||
path::PathBuf,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
pub mod mainnet;
|
||||
pub mod var_names;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct ChainDetails {
|
||||
pub bech32_account_prefix: String,
|
||||
@@ -28,6 +30,8 @@ pub struct NymContracts {
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
pub coconut_dkg_contract_address: Option<String>,
|
||||
pub service_provider_directory_contract_address: Option<String>,
|
||||
pub name_service_contract_address: Option<String>,
|
||||
}
|
||||
|
||||
// I wanted to use the simpler `NetworkDetails` name, but there's a clash
|
||||
@@ -68,6 +72,14 @@ impl NymNetworkDetails {
|
||||
}
|
||||
|
||||
pub fn new_from_env() -> Self {
|
||||
fn get_optional_env<K: AsRef<OsStr>>(env: K) -> Option<String> {
|
||||
match var(env) {
|
||||
Ok(var) => Some(var),
|
||||
Err(VarError::NotPresent) => None,
|
||||
err => panic!("Unable to set: {:?}", err),
|
||||
}
|
||||
}
|
||||
|
||||
NymNetworkDetails::new_empty()
|
||||
.with_bech32_account_prefix(
|
||||
var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"),
|
||||
@@ -117,6 +129,10 @@ impl NymNetworkDetails {
|
||||
.with_coconut_dkg_contract(Some(
|
||||
var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"),
|
||||
))
|
||||
.with_service_provider_directory_contract(get_optional_env(
|
||||
var_names::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_name_service_contract(get_optional_env(var_names::NAME_SERVICE_CONTRACT_ADDRESS))
|
||||
}
|
||||
|
||||
pub fn new_mainnet() -> Self {
|
||||
@@ -146,6 +162,8 @@ impl NymNetworkDetails {
|
||||
coconut_dkg_contract_address: parse_optional_str(
|
||||
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
),
|
||||
service_provider_directory_contract_address: None,
|
||||
name_service_contract_address: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -227,6 +245,21 @@ impl NymNetworkDetails {
|
||||
self.contracts.coconut_dkg_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_service_provider_directory_contract<S: Into<String>>(
|
||||
mut self,
|
||||
contract: Option<S>,
|
||||
) -> Self {
|
||||
self.contracts.service_provider_directory_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_name_service_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.name_service_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -20,10 +20,6 @@ pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str =
|
||||
pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/";
|
||||
|
||||
@@ -19,6 +19,9 @@ pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS";
|
||||
pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str =
|
||||
"SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS";
|
||||
pub const NAME_SERVICE_CONTRACT_ADDRESS: &str = "NAME_SERVICE_CONTRACT_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
|
||||
|
||||
@@ -46,4 +46,7 @@ pub enum NetworkTestingError {
|
||||
|
||||
#[error("received a packet that could not be reconstructed into a full message with a single fragment")]
|
||||
NonReconstructablePacket,
|
||||
|
||||
#[error("the recipient of the test packet was never specified")]
|
||||
UnknownPacketRecipient,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
pub mod error;
|
||||
pub mod message;
|
||||
pub mod node;
|
||||
pub mod processor;
|
||||
pub mod receiver;
|
||||
pub mod tester;
|
||||
|
||||
|
||||
@@ -2,27 +2,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::NetworkTestingError;
|
||||
use crate::MixId;
|
||||
use crate::node::TestableNode;
|
||||
use nym_sphinx::message::NymMessage;
|
||||
use nym_topology::{gateway, mix};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Clone, Copy)]
|
||||
pub enum NodeType {
|
||||
Mixnode(MixId),
|
||||
Gateway,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct Empty;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct TestMessage<T = Empty> {
|
||||
pub encoded_node_identity: String,
|
||||
pub node_owner: String,
|
||||
pub node_type: NodeType,
|
||||
pub tested_node: TestableNode,
|
||||
|
||||
pub msg_id: u32,
|
||||
pub total_msgs: u32,
|
||||
@@ -34,26 +25,72 @@ pub struct TestMessage<T = Empty> {
|
||||
}
|
||||
|
||||
impl<T> TestMessage<T> {
|
||||
pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
|
||||
pub fn new<N: Into<TestableNode>>(node: N, msg_id: u32, total_msgs: u32, ext: T) -> Self {
|
||||
TestMessage {
|
||||
encoded_node_identity: node.identity_key.to_base58_string(),
|
||||
node_owner: node.owner.clone(),
|
||||
node_type: NodeType::Mixnode(node.mix_id),
|
||||
tested_node: node.into(),
|
||||
msg_id,
|
||||
total_msgs,
|
||||
ext,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
|
||||
Self::new(node, msg_id, total_msgs, ext)
|
||||
}
|
||||
|
||||
pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
|
||||
TestMessage {
|
||||
encoded_node_identity: node.identity_key.to_base58_string(),
|
||||
node_owner: node.owner.clone(),
|
||||
node_type: NodeType::Gateway,
|
||||
msg_id,
|
||||
total_msgs,
|
||||
ext,
|
||||
Self::new(node, msg_id, total_msgs, ext)
|
||||
}
|
||||
|
||||
pub fn new_serialized<N>(
|
||||
node: N,
|
||||
msg_id: u32,
|
||||
total_msgs: u32,
|
||||
ext: T,
|
||||
) -> Result<Vec<u8>, NetworkTestingError>
|
||||
where
|
||||
N: Into<TestableNode>,
|
||||
T: Serialize,
|
||||
{
|
||||
Self::new(node, msg_id, total_msgs, ext).as_bytes()
|
||||
}
|
||||
|
||||
pub fn new_plaintexts<N>(
|
||||
node: &N,
|
||||
total_msgs: u32,
|
||||
ext: T,
|
||||
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
|
||||
where
|
||||
for<'a> &'a N: Into<TestableNode>,
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
let mut msgs = Vec::with_capacity(total_msgs as usize);
|
||||
for msg_id in 1..=total_msgs {
|
||||
msgs.push(Self::new(node, msg_id, total_msgs, ext.clone()).as_bytes()?)
|
||||
}
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
pub fn mix_plaintexts(
|
||||
node: &mix::Node,
|
||||
total_msgs: u32,
|
||||
ext: T,
|
||||
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
Self::new_plaintexts(node, total_msgs, ext)
|
||||
}
|
||||
|
||||
pub fn gateway_plaintexts(
|
||||
node: &gateway::Node,
|
||||
total_msgs: u32,
|
||||
ext: T,
|
||||
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
Self::new_plaintexts(node, total_msgs, ext)
|
||||
}
|
||||
|
||||
pub fn as_json_string(&self) -> Result<String, NetworkTestingError>
|
||||
@@ -88,12 +125,3 @@ impl<T> TestMessage<T> {
|
||||
.map_err(|source| NetworkTestingError::MalformedTestMessageReceived { source })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for TestMessage<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.encoded_node_identity.hash(state);
|
||||
self.node_owner.hash(state);
|
||||
self.node_type.hash(state);
|
||||
self.ext.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::MixId;
|
||||
use nym_topology::{gateway, mix};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct TestableNode {
|
||||
pub encoded_identity: String,
|
||||
pub owner: String,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub typ: NodeType,
|
||||
}
|
||||
|
||||
impl TestableNode {
|
||||
pub fn new(encoded_identity: String, owner: String, typ: NodeType) -> Self {
|
||||
TestableNode {
|
||||
encoded_identity,
|
||||
owner,
|
||||
typ,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mixnode(encoded_identity: String, owner: String, mix_id: MixId) -> Self {
|
||||
TestableNode::new(encoded_identity, owner, NodeType::Mixnode { mix_id })
|
||||
}
|
||||
|
||||
pub fn new_gateway(encoded_identity: String, owner: String) -> Self {
|
||||
TestableNode::new(encoded_identity, owner, NodeType::Gateway)
|
||||
}
|
||||
|
||||
pub fn is_mixnode(&self) -> bool {
|
||||
self.typ.is_mixnode()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mix::Node> for TestableNode {
|
||||
fn from(value: &'a mix::Node) -> Self {
|
||||
TestableNode {
|
||||
encoded_identity: value.identity_key.to_base58_string(),
|
||||
owner: value.owner.clone(),
|
||||
typ: NodeType::Mixnode {
|
||||
mix_id: value.mix_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a gateway::Node> for TestableNode {
|
||||
fn from(value: &'a gateway::Node) -> Self {
|
||||
TestableNode {
|
||||
encoded_identity: value.identity_key.to_base58_string(),
|
||||
owner: value.owner.clone(),
|
||||
typ: NodeType::Gateway,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestableNode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {} owned by {}",
|
||||
self.typ, self.encoded_identity, self.owner
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NodeType {
|
||||
Mixnode { mix_id: MixId },
|
||||
Gateway,
|
||||
}
|
||||
|
||||
impl NodeType {
|
||||
pub fn is_mixnode(&self) -> bool {
|
||||
matches!(self, NodeType::Mixnode { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NodeType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NodeType::Mixnode { mix_id } => write!(f, "mixnode (mix_id {mix_id})"),
|
||||
NodeType::Gateway => write!(f, "gateway"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::NetworkTestingError;
|
||||
use crate::TestMessage;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_sphinx::acknowledgements::identifier::recover_identifier;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
// simple enum containing aggregated processed results
|
||||
pub enum Received<T> {
|
||||
Message(TestMessage<T>),
|
||||
Ack(FragmentIdentifier),
|
||||
}
|
||||
|
||||
impl<T> From<TestMessage<T>> for Received<T> {
|
||||
fn from(value: TestMessage<T>) -> Self {
|
||||
Received::Message(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<FragmentIdentifier> for Received<T> {
|
||||
fn from(value: FragmentIdentifier) -> Self {
|
||||
Received::Ack(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPacketProcessor<T, R: MessageReceiver = SphinxMessageReceiver> {
|
||||
local_encryption_keypair: Arc<encryption::KeyPair>,
|
||||
ack_key: Arc<AckKey>,
|
||||
|
||||
/// Structure responsible for decrypting and recovering plaintext message from received ciphertexts.
|
||||
message_receiver: R,
|
||||
|
||||
_ext_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TestPacketProcessor<T, SphinxMessageReceiver> {
|
||||
pub fn new_sphinx_processor(
|
||||
local_encryption_keypair: Arc<encryption::KeyPair>,
|
||||
ack_key: Arc<AckKey>,
|
||||
) -> Self {
|
||||
Self::new(local_encryption_keypair, ack_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> TestPacketProcessor<T, R>
|
||||
where
|
||||
R: MessageReceiver,
|
||||
{
|
||||
pub fn new(local_encryption_keypair: Arc<encryption::KeyPair>, ack_key: Arc<AckKey>) -> Self {
|
||||
TestPacketProcessor {
|
||||
local_encryption_keypair,
|
||||
ack_key,
|
||||
message_receiver: R::new(),
|
||||
_ext_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_mixnet_message(
|
||||
&mut self,
|
||||
mut raw_message: Vec<u8>,
|
||||
) -> Result<TestMessage<T>, NetworkTestingError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let plaintext = self
|
||||
.message_receiver
|
||||
.recover_plaintext_from_regular_packet(
|
||||
self.local_encryption_keypair.private_key(),
|
||||
&mut raw_message,
|
||||
)?;
|
||||
let fragment = self.message_receiver.recover_fragment(plaintext)?;
|
||||
|
||||
// test messages must consist of a single fragment
|
||||
let (serialized, _) = self
|
||||
.message_receiver
|
||||
.insert_new_fragment(fragment)?
|
||||
.ok_or(NetworkTestingError::NonReconstructablePacket)?;
|
||||
|
||||
TestMessage::try_recover(serialized)
|
||||
}
|
||||
|
||||
pub fn process_ack(
|
||||
&mut self,
|
||||
raw_ack: Vec<u8>,
|
||||
) -> Result<FragmentIdentifier, NetworkTestingError> {
|
||||
let serialized_ack = recover_identifier(&self.ack_key, &raw_ack)
|
||||
.ok_or(NetworkTestingError::UnrecoverableAck)?;
|
||||
|
||||
FragmentIdentifier::try_from_bytes(serialized_ack)
|
||||
.map_err(|source| NetworkTestingError::MalformedAckIdentifier { source })
|
||||
}
|
||||
}
|
||||
@@ -2,114 +2,95 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::NetworkTestingError;
|
||||
use crate::processor::{Received, TestPacketProcessor};
|
||||
use crate::{log_err, log_info, log_warn};
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_sphinx::message::NymMessage;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver};
|
||||
use nym_sphinx::{
|
||||
acknowledgements::{identifier::recover_identifier, AckKey},
|
||||
chunking::fragment::FragmentIdentifier,
|
||||
};
|
||||
use nym_task::TaskClient;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type ReceivedSender = mpsc::UnboundedSender<Received>;
|
||||
pub type ReceivedReceiver = mpsc::UnboundedReceiver<Received>;
|
||||
|
||||
// simple enum containing aggregated processed results
|
||||
pub enum Received {
|
||||
Message(NymMessage),
|
||||
Ack(FragmentIdentifier),
|
||||
}
|
||||
|
||||
impl From<NymMessage> for Received {
|
||||
fn from(value: NymMessage) -> Self {
|
||||
Received::Message(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FragmentIdentifier> for Received {
|
||||
fn from(value: FragmentIdentifier) -> Self {
|
||||
Received::Ack(value)
|
||||
}
|
||||
}
|
||||
pub type ReceivedSender<T> = mpsc::UnboundedSender<Received<T>>;
|
||||
pub type ReceivedReceiver<T> = mpsc::UnboundedReceiver<Received<T>>;
|
||||
|
||||
// the 'Simple' bit comes from the fact that it expects all received messages to consist of a single `Fragment`
|
||||
pub struct SimpleMessageReceiver<R: MessageReceiver = SphinxMessageReceiver> {
|
||||
local_encryption_keypair: Arc<encryption::KeyPair>,
|
||||
|
||||
ack_key: Arc<AckKey>,
|
||||
|
||||
/// Structure responsible for decrypting and recovering plaintext message from received ciphertexts.
|
||||
message_receiver: R,
|
||||
pub struct SimpleMessageReceiver<T, R: MessageReceiver = SphinxMessageReceiver> {
|
||||
message_processor: TestPacketProcessor<T, R>,
|
||||
|
||||
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
|
||||
received_sender: ReceivedSender,
|
||||
received_sender: ReceivedSender<T>,
|
||||
shutdown: TaskClient,
|
||||
}
|
||||
|
||||
impl SimpleMessageReceiver<SphinxMessageReceiver> {
|
||||
impl<T> SimpleMessageReceiver<T, SphinxMessageReceiver> {
|
||||
pub fn new_sphinx_receiver(
|
||||
local_encryption_keypair: Arc<encryption::KeyPair>,
|
||||
ack_key: Arc<AckKey>,
|
||||
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
received_sender: ReceivedSender,
|
||||
received_sender: ReceivedSender<T>,
|
||||
shutdown: TaskClient,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
local_encryption_keypair,
|
||||
ack_key,
|
||||
mixnet_message_receiver,
|
||||
acks_receiver,
|
||||
received_sender,
|
||||
shutdown,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R: MessageReceiver> SimpleMessageReceiver<T, R> {
|
||||
pub fn new(
|
||||
local_encryption_keypair: Arc<encryption::KeyPair>,
|
||||
ack_key: Arc<AckKey>,
|
||||
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
|
||||
received_sender: ReceivedSender<T>,
|
||||
shutdown: TaskClient,
|
||||
) -> Self {
|
||||
SimpleMessageReceiver {
|
||||
local_encryption_keypair,
|
||||
ack_key,
|
||||
message_receiver: SphinxMessageReceiver::new(),
|
||||
message_processor: TestPacketProcessor::new(local_encryption_keypair, ack_key),
|
||||
mixnet_message_receiver,
|
||||
acks_receiver,
|
||||
received_sender,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: MessageReceiver> SimpleMessageReceiver<R> {
|
||||
fn forward_received<T: Into<Received>>(&self, received: T) {
|
||||
fn forward_received<U: Into<Received<T>>>(&self, received: U) {
|
||||
// TODO: remove the unwrap once/if we do graceful shutdowns here
|
||||
self.received_sender
|
||||
.unbounded_send(received.into())
|
||||
.expect("ReceivedReceiver has stopped receiving");
|
||||
}
|
||||
|
||||
fn on_mixnet_message(&mut self, mut raw_message: Vec<u8>) -> Result<(), NetworkTestingError> {
|
||||
let plaintext = self
|
||||
.message_receiver
|
||||
.recover_plaintext_from_regular_packet(
|
||||
self.local_encryption_keypair.private_key(),
|
||||
&mut raw_message,
|
||||
)?;
|
||||
let fragment = self.message_receiver.recover_fragment(plaintext)?;
|
||||
let (recovered, _) = self
|
||||
.message_receiver
|
||||
.insert_new_fragment(fragment)?
|
||||
.ok_or(NetworkTestingError::NonReconstructablePacket)?; // by definition of this receiver, the message must consist of a single fragment
|
||||
|
||||
fn on_mixnet_message(&mut self, raw_message: Vec<u8>) -> Result<(), NetworkTestingError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let recovered = self.message_processor.process_mixnet_message(raw_message)?;
|
||||
self.forward_received(recovered);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_ack(&mut self, raw_ack: Vec<u8>) -> Result<(), NetworkTestingError> {
|
||||
let serialized_ack = recover_identifier(&self.ack_key, &raw_ack)
|
||||
.ok_or(NetworkTestingError::UnrecoverableAck)?;
|
||||
|
||||
let frag_id = FragmentIdentifier::try_from_bytes(serialized_ack)
|
||||
.map_err(|source| NetworkTestingError::MalformedAckIdentifier { source })?;
|
||||
|
||||
let frag_id = self.message_processor.process_ack(raw_ack)?;
|
||||
self.forward_received(frag_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
pub async fn run(&mut self)
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
while !self.shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
@@ -21,7 +21,10 @@ pub struct NodeTester<R> {
|
||||
|
||||
base_topology: NymTopology,
|
||||
|
||||
recipient: Recipient,
|
||||
/// Generally test packets are designed to be sent from ourselves to ourselves,
|
||||
/// However, one might want to customise this behaviour.
|
||||
/// In that case an explicit `Recipient` has to be provided when constructing test packets.
|
||||
self_address: Option<Recipient>,
|
||||
|
||||
packet_size: PacketSize,
|
||||
|
||||
@@ -47,7 +50,7 @@ where
|
||||
pub fn new(
|
||||
rng: R,
|
||||
base_topology: NymTopology,
|
||||
recipient: Recipient,
|
||||
self_address: Option<Recipient>,
|
||||
packet_size: PacketSize,
|
||||
average_packet_delay: Duration,
|
||||
average_ack_delay: Duration,
|
||||
@@ -56,7 +59,7 @@ where
|
||||
Self {
|
||||
rng,
|
||||
base_topology,
|
||||
recipient,
|
||||
self_address,
|
||||
packet_size,
|
||||
average_packet_delay,
|
||||
average_ack_delay,
|
||||
@@ -89,7 +92,7 @@ where
|
||||
mix: &mix::Node,
|
||||
test_packets: u32,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError> {
|
||||
self.mixnode_test_packets(mix, Empty, test_packets)
|
||||
self.mixnode_test_packets(mix, Empty, test_packets, None)
|
||||
}
|
||||
|
||||
pub fn mixnode_test_packets<T>(
|
||||
@@ -97,6 +100,7 @@ where
|
||||
mix: &mix::Node,
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
@@ -104,9 +108,35 @@ where
|
||||
let ephemeral_topology = self.testable_mix_topology(mix);
|
||||
|
||||
let mut packets = Vec::with_capacity(test_packets as usize);
|
||||
for i in 1..=test_packets {
|
||||
let msg = TestMessage::new_mix(mix, i, test_packets, msg_ext.clone());
|
||||
packets.push(self.create_test_packet(&msg, &ephemeral_topology)?);
|
||||
for plaintext in TestMessage::mix_plaintexts(mix, test_packets, msg_ext)? {
|
||||
packets.push(self.wrap_plaintext_data(
|
||||
plaintext,
|
||||
&ephemeral_topology,
|
||||
custom_recipient,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(packets)
|
||||
}
|
||||
|
||||
pub fn mixnodes_test_packets<T>(
|
||||
&mut self,
|
||||
nodes: &[mix::Node],
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
let mut packets = Vec::new();
|
||||
for node in nodes {
|
||||
packets.append(&mut self.mixnode_test_packets(
|
||||
node,
|
||||
msg_ext.clone(),
|
||||
test_packets,
|
||||
custom_recipient,
|
||||
)?)
|
||||
}
|
||||
|
||||
Ok(packets)
|
||||
@@ -117,6 +147,7 @@ where
|
||||
mix_id: MixId,
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
@@ -125,7 +156,7 @@ where
|
||||
return Err(NetworkTestingError::NonExistentMixnode {mix_id})
|
||||
};
|
||||
|
||||
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets)
|
||||
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
|
||||
}
|
||||
|
||||
pub fn existing_identity_mixnode_test_packets<T>(
|
||||
@@ -133,6 +164,7 @@ where
|
||||
encoded_mix_identity: String,
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
@@ -141,19 +173,57 @@ where
|
||||
return Err(NetworkTestingError::NonExistentMixnodeIdentity { mix_identity: encoded_mix_identity })
|
||||
};
|
||||
|
||||
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets)
|
||||
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
|
||||
}
|
||||
|
||||
pub fn create_test_packet<T>(
|
||||
pub fn gateway_test_packets<T>(
|
||||
&mut self,
|
||||
message: &TestMessage<T>,
|
||||
topology: &NymTopology,
|
||||
) -> Result<PreparedFragment, NetworkTestingError>
|
||||
gateway: &gateway::Node,
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
let serialized = message.as_bytes()?;
|
||||
let message = NymMessage::new_plain(serialized);
|
||||
let ephemeral_topology = self.testable_gateway_topology(gateway);
|
||||
|
||||
let mut packets = Vec::with_capacity(test_packets as usize);
|
||||
for plaintext in TestMessage::gateway_plaintexts(gateway, test_packets, msg_ext)? {
|
||||
packets.push(self.wrap_plaintext_data(
|
||||
plaintext,
|
||||
&ephemeral_topology,
|
||||
custom_recipient,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(packets)
|
||||
}
|
||||
|
||||
pub fn existing_gateway_test_packets<T>(
|
||||
&mut self,
|
||||
encoded_gateway_identity: String,
|
||||
msg_ext: T,
|
||||
test_packets: u32,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
{
|
||||
let Some(node) = self.base_topology.find_gateway(&encoded_gateway_identity) else {
|
||||
return Err(NetworkTestingError::NonExistentGateway { gateway_identity: encoded_gateway_identity })
|
||||
};
|
||||
|
||||
self.gateway_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
|
||||
}
|
||||
|
||||
pub fn wrap_plaintext_data(
|
||||
&mut self,
|
||||
plaintext: Vec<u8>,
|
||||
topology: &NymTopology,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<PreparedFragment, NetworkTestingError> {
|
||||
let message = NymMessage::new_plain(plaintext);
|
||||
|
||||
let mut fragments = self.pad_and_split_message(message, self.packet_size);
|
||||
|
||||
@@ -165,13 +235,29 @@ where
|
||||
// we would have returned the error when checking for its length
|
||||
let fragment = fragments.pop().unwrap();
|
||||
|
||||
// the packet is designed to be sent from ourselves to ourselves
|
||||
let address = self.recipient;
|
||||
// either `self_address` or `custom_recipient` has to be specified.
|
||||
let address = custom_recipient.unwrap_or(
|
||||
self.self_address
|
||||
.ok_or(NetworkTestingError::UnknownPacketRecipient)?,
|
||||
);
|
||||
|
||||
// TODO: can we avoid this arc clone?
|
||||
let ack_key = Arc::clone(&self.ack_key);
|
||||
Ok(self.prepare_chunk_for_sending(fragment, topology, &ack_key, &address, &address)?)
|
||||
}
|
||||
|
||||
pub fn create_test_packet<T>(
|
||||
&mut self,
|
||||
message: &TestMessage<T>,
|
||||
topology: &NymTopology,
|
||||
custom_recipient: Option<Recipient>,
|
||||
) -> Result<PreparedFragment, NetworkTestingError>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let serialized = message.as_bytes()?;
|
||||
self.wrap_plaintext_data(serialized, topology, custom_recipient)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: CryptoRng + Rng> FragmentPreparer for NodeTester<R> {
|
||||
|
||||
@@ -20,11 +20,12 @@ nym-sphinx-chunking = { path = "chunking" }
|
||||
nym-sphinx-cover = { path = "cover" }
|
||||
nym-sphinx-forwarding = { path = "forwarding" }
|
||||
nym-sphinx-params = { path = "params" }
|
||||
nym-sphinx-routing = { path = "routing" }
|
||||
nym-sphinx-types = { path = "types" }
|
||||
|
||||
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
|
||||
# to separate crate?
|
||||
nym-crypto = { path = "../crypto", version = "0.2.0" }
|
||||
nym-crypto = { path = "../crypto", version = "0.3.0" }
|
||||
nym-topology = { path = "../topology" }
|
||||
|
||||
# outfox
|
||||
@@ -32,7 +33,7 @@ nym-outfox = { path = "../../nym-outfox" }
|
||||
|
||||
[dev-dependencies]
|
||||
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-crypto = { path = "../crypto", version = "0.2.0", features = ["asymmetric"] }
|
||||
nym-crypto = { path = "../crypto", version = "0.3.0", features = ["asymmetric"] }
|
||||
|
||||
# do not include this when compiling into wasm as it somehow when combined together with reqwest, it will require
|
||||
# net2 via tokio-util -> tokio -> mio -> net2
|
||||
|
||||
@@ -60,6 +60,7 @@ impl SurbAck {
|
||||
|
||||
let surb_ack_packet = SphinxPacketBuilder::new()
|
||||
.with_payload_size(PacketSize::AckPacket.payload_size())
|
||||
.with_initial_secret(&[42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42].into())
|
||||
.build_packet(surb_ack_payload, &route, &destination, &delays)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ where
|
||||
// once merged, that's an easy rng injection point for sphinx packets : )
|
||||
let packet = SphinxPacketBuilder::new()
|
||||
.with_payload_size(packet_size.payload_size())
|
||||
.with_initial_secret(&[42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42].into())
|
||||
.build_packet(packet_payload, &route, &destination, &delays)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "nym-sphinx-routing"
|
||||
version = "0.1.0"
|
||||
description = "Sphinx packet routing as Nym mix packets"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-types = { path = "../types" }
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_types::Node;
|
||||
use thiserror::Error;
|
||||
|
||||
pub trait SphinxRouteMaker {
|
||||
type Error;
|
||||
|
||||
fn sphinx_route(&mut self, hops: u8, destination: &Recipient)
|
||||
-> Result<Vec<Node>, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Clone, Copy)]
|
||||
#[error("the route vector contains {available} nodes while {requested} hops are required")]
|
||||
pub struct InvalidNumberOfHops {
|
||||
available: usize,
|
||||
requested: u8,
|
||||
}
|
||||
|
||||
// if one wants to provide a hardcoded route, they can
|
||||
impl SphinxRouteMaker for Vec<Node> {
|
||||
type Error = InvalidNumberOfHops;
|
||||
|
||||
fn sphinx_route(
|
||||
&mut self,
|
||||
hops: u8,
|
||||
_destination: &Recipient,
|
||||
) -> Result<Vec<Node>, InvalidNumberOfHops> {
|
||||
// it's the responsibility of the caller to ensure the hardcoded route has correct number of hops
|
||||
// and that it's final hop include the recipient's gateway.
|
||||
|
||||
if self.len() != hops as usize {
|
||||
Err(InvalidNumberOfHops {
|
||||
available: self.len(),
|
||||
requested: hops,
|
||||
})
|
||||
} else {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ pub use nym_sphinx_anonymous_replies as anonymous_replies;
|
||||
pub use nym_sphinx_chunking as chunking;
|
||||
pub use nym_sphinx_cover as cover;
|
||||
pub use nym_sphinx_forwarding as forwarding;
|
||||
pub use nym_sphinx_params as params;
|
||||
pub use nym_sphinx_routing as routing;
|
||||
pub use nym_sphinx_types::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use nym_sphinx_framing as framing;
|
||||
pub use nym_sphinx_params as params;
|
||||
pub use nym_sphinx_types::*;
|
||||
|
||||
// TEMP UNTIL FURTHER REFACTORING
|
||||
pub use preparer::payload::NymsphinxPayloadBuilder;
|
||||
|
||||
@@ -38,6 +38,12 @@ pub struct PreparedFragment {
|
||||
pub fragment_identifier: FragmentIdentifier,
|
||||
}
|
||||
|
||||
impl From<PreparedFragment> for MixPacket {
|
||||
fn from(value: PreparedFragment) -> Self {
|
||||
value.mix_packet
|
||||
}
|
||||
}
|
||||
|
||||
// this is extracted into a trait with default implementation to remove duplicate code
|
||||
// (which we REALLY want to avoid with crypto)
|
||||
pub trait FragmentPreparer {
|
||||
@@ -202,6 +208,7 @@ pub trait FragmentPreparer {
|
||||
// there's absolutely no reason for this call to fail.
|
||||
let sphinx_packet = SphinxPacketBuilder::new()
|
||||
.with_payload_size(packet_size.payload_size())
|
||||
.with_initial_secret(&[42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42].into())
|
||||
.build_packet(packet_payload, &route, &destination, &delays)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
sphinx-packet = { version = "0.1.0" }
|
||||
#sphinx-packet = { version = "0.1.0" }
|
||||
sphinx-packet = { git = "https://github.com/nymtech/sphinx.git", branch = "simon/key-reuse"}
|
||||
|
||||
#[patch.crates-io]
|
||||
#sphinx-packet = { path = "../../../../sphinx" }
|
||||
|
||||
@@ -8,7 +8,7 @@ pub use sphinx_packet::{
|
||||
PAYLOAD_KEY_SIZE,
|
||||
},
|
||||
crypto::{self, EphemeralSecret, PrivateKey, PublicKey, SharedSecret},
|
||||
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE},
|
||||
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE, keys::RoutingKeys},
|
||||
packet::builder::{self, DEFAULT_PAYLOAD_SIZE},
|
||||
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "nym-store-cipher"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aes-gcm = { version = "0.10.1" }
|
||||
argon2 = { version = "0.5.0" }
|
||||
rand = "0.8.5"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
zeroize = { version = "1.6.0", features = ["zeroize_derive"] }
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
json = ["serde_json"]
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use aes_gcm::aead::{Aead, Nonce};
|
||||
use aes_gcm::{AeadCore, AeadInPlace, Key, KeyInit, KeySizeUser};
|
||||
use rand::{thread_rng, CryptoRng, Fill, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_helpers::{argon2_algorithm_helper, argon2_params_helper, argon2_version_helper};
|
||||
use thiserror::Error;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub use aes_gcm::Aes256Gcm;
|
||||
pub use argon2::{Algorithm, Argon2, Params, Version};
|
||||
|
||||
mod serde_helpers;
|
||||
|
||||
pub const CURRENT_VERSION: u8 = 1;
|
||||
pub const ARGON2_SALT_SIZE: usize = 16;
|
||||
pub const AES256GCM_NONCE_SIZE: usize = 12;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("failed to encrypt/decrypt provided data: {cause}")]
|
||||
AesFailure { cause: aes_gcm::Error },
|
||||
|
||||
#[error("failed to expand the passphrase: {cause}")]
|
||||
Argon2Failure { cause: argon2::Error },
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
#[error("failed to serialize/deserialize JSON: {source}")]
|
||||
SerdeJsonFailure {
|
||||
#[from]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to generate random bytes: {source}")]
|
||||
RandomError {
|
||||
#[from]
|
||||
source: rand::Error,
|
||||
},
|
||||
|
||||
#[error("the received ciphertext was encrypted with different store version ({received}). The current version is {CURRENT_VERSION}")]
|
||||
VersionMismatch { received: u8 },
|
||||
}
|
||||
|
||||
// it's weird that this couldn't be auto-derived with a `#[from]`...
|
||||
impl From<aes_gcm::Error> for Error {
|
||||
fn from(cause: aes_gcm::Error) -> Self {
|
||||
Error::AesFailure { cause }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<argon2::Error> for Error {
|
||||
fn from(cause: argon2::Error) -> Self {
|
||||
Error::Argon2Failure { cause }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum KdfInfo {
|
||||
Argon2 {
|
||||
/// The Argon2 parameters that were used when deriving the store key.
|
||||
#[serde(with = "argon2_params_helper")]
|
||||
params: Params,
|
||||
|
||||
/// The specific Argon2 algorithm variant used when deriving the store key.
|
||||
#[serde(with = "argon2_algorithm_helper")]
|
||||
algorithm: Algorithm,
|
||||
|
||||
/// The specific version of the Argon2 algorithm used when deriving the store key.
|
||||
#[serde(with = "argon2_version_helper")]
|
||||
version: Version,
|
||||
|
||||
/// The salt that was used when the passphrase was expanded into a store key.
|
||||
kdf_salt: [u8; ARGON2_SALT_SIZE],
|
||||
},
|
||||
}
|
||||
|
||||
impl KdfInfo {
|
||||
pub fn expand_key<C>(&self, passphrase: &[u8]) -> Result<Key<C>, Error>
|
||||
where
|
||||
C: KeySizeUser,
|
||||
{
|
||||
match self {
|
||||
KdfInfo::Argon2 {
|
||||
params,
|
||||
algorithm,
|
||||
version,
|
||||
kdf_salt,
|
||||
} => argon2_derive_cipher_key::<C>(
|
||||
passphrase,
|
||||
kdf_salt,
|
||||
&[],
|
||||
params.clone(),
|
||||
*algorithm,
|
||||
*version,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_default_settings() -> Result<Self, Error> {
|
||||
let kdf_salt = Self::random_salt()?;
|
||||
Ok(KdfInfo::Argon2 {
|
||||
params: Default::default(),
|
||||
algorithm: Default::default(),
|
||||
version: Default::default(),
|
||||
kdf_salt,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn random_salt() -> Result<[u8; ARGON2_SALT_SIZE], Error> {
|
||||
let mut rng = thread_rng();
|
||||
Self::random_salt_with_rng(&mut rng)
|
||||
}
|
||||
|
||||
pub fn random_salt_with_rng<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
) -> Result<[u8; ARGON2_SALT_SIZE], Error> {
|
||||
let mut salt = [0u8; ARGON2_SALT_SIZE];
|
||||
salt.try_fill(rng)?;
|
||||
Ok(salt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct StoreCipher<C = Aes256Gcm>
|
||||
where
|
||||
C: KeySizeUser,
|
||||
{
|
||||
key: Key<C>,
|
||||
}
|
||||
|
||||
impl<C: KeySizeUser + KeyInit> StoreCipher<C>
|
||||
where
|
||||
C: KeySizeUser + KeyInit,
|
||||
{
|
||||
pub fn new(passphrase: &[u8], kdf_info: KdfInfo) -> Result<Self, Error> {
|
||||
let key = kdf_info.expand_key::<C>(passphrase)?;
|
||||
Ok(StoreCipher { key })
|
||||
}
|
||||
|
||||
pub fn new_with_default_kdf(passphrase: &[u8]) -> Result<Self, Error> {
|
||||
let kdf_info = KdfInfo::new_with_default_settings()?;
|
||||
Self::new(passphrase, kdf_info)
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub fn encrypt_json_value<T: Serialize>(&self, data: &T) -> Result<EncryptedData, Error>
|
||||
where
|
||||
C: AeadInPlace,
|
||||
{
|
||||
let raw = serde_json::to_vec(data)?;
|
||||
self.encrypt_data(raw)
|
||||
}
|
||||
|
||||
// Unless you know what you're doing, use `Self::encrypt_data` instead.
|
||||
// As the caller of this method needs to make sure to correctly dispose of the original plaintext.
|
||||
pub fn encrypt_data_ref(&self, data: &[u8]) -> Result<EncryptedData, Error>
|
||||
where
|
||||
C: Aead,
|
||||
{
|
||||
let nonce = Self::random_nonce()?;
|
||||
|
||||
let cipher = C::new(&self.key);
|
||||
let ciphertext = cipher.encrypt(&nonce, data)?;
|
||||
|
||||
Ok(EncryptedData {
|
||||
version: CURRENT_VERSION,
|
||||
ciphertext,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_data(&self, mut data: Vec<u8>) -> Result<EncryptedData, Error>
|
||||
where
|
||||
C: AeadInPlace,
|
||||
{
|
||||
let nonce = Self::random_nonce()?;
|
||||
|
||||
let cipher = C::new(&self.key);
|
||||
cipher.encrypt_in_place(&nonce, &[], &mut data)?;
|
||||
|
||||
Ok(EncryptedData {
|
||||
version: CURRENT_VERSION,
|
||||
ciphertext: data,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub fn decrypt_json_value<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
data: EncryptedData,
|
||||
) -> Result<T, Error>
|
||||
where
|
||||
C: AeadInPlace,
|
||||
{
|
||||
let mut plaintext = self.decrypt_data(data)?;
|
||||
|
||||
// DO NOT USE `?` here!!
|
||||
// We need to make sure to zeroize the plaintext even if deserialization fails!!
|
||||
let value = serde_json::from_slice(&plaintext);
|
||||
|
||||
plaintext.zeroize();
|
||||
Ok(value?)
|
||||
}
|
||||
|
||||
pub fn decrypt_data_unchecked(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
|
||||
where
|
||||
C: Aead,
|
||||
{
|
||||
let cipher = C::new(&self.key);
|
||||
let plaintext = cipher.decrypt(
|
||||
Nonce::<C>::from_slice(&data.nonce),
|
||||
data.ciphertext.as_ref(),
|
||||
)?;
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
pub fn decrypt_data(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
|
||||
where
|
||||
C: Aead,
|
||||
{
|
||||
if data.version != CURRENT_VERSION {
|
||||
return Err(Error::VersionMismatch {
|
||||
received: data.version,
|
||||
});
|
||||
}
|
||||
|
||||
self.decrypt_data_unchecked(data)
|
||||
}
|
||||
|
||||
pub fn random_nonce() -> Result<Nonce<C>, Error>
|
||||
where
|
||||
C: AeadCore,
|
||||
{
|
||||
let mut rng = thread_rng();
|
||||
Self::random_nonce_with_rng(&mut rng)
|
||||
}
|
||||
|
||||
pub fn random_nonce_with_rng<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Nonce<C>, Error>
|
||||
where
|
||||
C: AeadCore,
|
||||
{
|
||||
let mut nonce = Nonce::<C>::default();
|
||||
nonce.try_fill(rng)?;
|
||||
Ok(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct EncryptedData {
|
||||
pub version: u8,
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn argon2_derive_cipher_key<C>(
|
||||
passphrase: &[u8],
|
||||
salt: &[u8],
|
||||
pepper: &[u8],
|
||||
params: Params,
|
||||
algorithm: Algorithm,
|
||||
version: Version,
|
||||
) -> Result<Key<C>, Error>
|
||||
where
|
||||
C: KeySizeUser,
|
||||
{
|
||||
let argon2 = if pepper.is_empty() {
|
||||
Argon2::new(algorithm, version, params)
|
||||
} else {
|
||||
Argon2::new_with_secret(pepper, algorithm, version, params)?
|
||||
};
|
||||
|
||||
let mut key = Key::<C>::default();
|
||||
argon2.hash_password_into(passphrase, salt, &mut key)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod argon2_params_helper {
|
||||
use argon2::Params;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Refer to [argon2::Params] for details.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Argon2Params {
|
||||
m_cost: u32,
|
||||
t_cost: u32,
|
||||
p_cost: u32,
|
||||
output_len: Option<usize>,
|
||||
// Note: `keyid` and `data` are not longer part of the argon2 standard
|
||||
// (see: <https://github.com/P-H-C/phc-winner-argon2/pull/173>), and should
|
||||
// not be used for any non-legacy work.
|
||||
// So we're explicitly skipping them for serialization
|
||||
}
|
||||
|
||||
impl From<&Params> for Argon2Params {
|
||||
fn from(value: &Params) -> Self {
|
||||
Argon2Params {
|
||||
m_cost: value.m_cost(),
|
||||
t_cost: value.t_cost(),
|
||||
p_cost: value.p_cost(),
|
||||
output_len: value.output_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Argon2Params> for Params {
|
||||
type Error = argon2::Error;
|
||||
|
||||
fn try_from(value: Argon2Params) -> Result<Self, Self::Error> {
|
||||
Params::new(value.m_cost, value.t_cost, value.p_cost, value.output_len)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(params: &Params, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
Argon2Params::from(params).serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Params, D::Error> {
|
||||
<Argon2Params>::deserialize(deserializer)?
|
||||
.try_into()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod argon2_algorithm_helper {
|
||||
use argon2::Algorithm;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Refer to [argon2::Algorithm] for details.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum Argon2Algorithm {
|
||||
Argon2d = 0,
|
||||
Argon2i = 1,
|
||||
Argon2id = 2,
|
||||
}
|
||||
|
||||
impl From<Algorithm> for Argon2Algorithm {
|
||||
fn from(value: Algorithm) -> Self {
|
||||
match value {
|
||||
Algorithm::Argon2d => Argon2Algorithm::Argon2d,
|
||||
Algorithm::Argon2i => Argon2Algorithm::Argon2i,
|
||||
Algorithm::Argon2id => Argon2Algorithm::Argon2id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Argon2Algorithm> for Algorithm {
|
||||
fn from(value: Argon2Algorithm) -> Self {
|
||||
match value {
|
||||
Argon2Algorithm::Argon2d => Algorithm::Argon2d,
|
||||
Argon2Algorithm::Argon2i => Algorithm::Argon2i,
|
||||
Argon2Algorithm::Argon2id => Algorithm::Argon2id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
algorithm: &Algorithm,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
Argon2Algorithm::from(*algorithm).serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Algorithm, D::Error> {
|
||||
<Argon2Algorithm>::deserialize(deserializer).map(From::from)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod argon2_version_helper {
|
||||
use argon2::Version;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Refer to [argon2::Version] for details.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[repr(u32)]
|
||||
enum Argon2Version {
|
||||
V0x10 = 0x10,
|
||||
V0x13 = 0x13,
|
||||
}
|
||||
|
||||
impl From<Version> for Argon2Version {
|
||||
fn from(value: Version) -> Self {
|
||||
match value {
|
||||
Version::V0x10 => Argon2Version::V0x10,
|
||||
Version::V0x13 => Argon2Version::V0x13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Argon2Version> for Version {
|
||||
fn from(value: Argon2Version) -> Self {
|
||||
match value {
|
||||
Argon2Version::V0x10 => Version::V0x10,
|
||||
Argon2Version::V0x13 => Version::V0x13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(algorithm: &Version, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
Argon2Version::from(*algorithm).serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Version, D::Error> {
|
||||
<Argon2Version>::deserialize(deserializer).map(From::from)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ nym-crypto = { path = "../crypto" }
|
||||
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types" }
|
||||
nym-sphinx-routing = { path = "../nymsphinx/routing" }
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::MixLayer;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum NymTopologyError {
|
||||
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
||||
EmptyNetworkTopology,
|
||||
|
||||
#[error("The provided network topology has no gateways available")]
|
||||
NoGatewaysAvailable,
|
||||
|
||||
#[error("The provided network topology has no mixnodes available")]
|
||||
NoMixnodesAvailable,
|
||||
|
||||
#[error("Gateway with identity key {identity_key} doesn't exist")]
|
||||
NonExistentGatewayError { identity_key: String },
|
||||
|
||||
#[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")]
|
||||
InvalidNumberOfHopsError { available: usize, requested: usize },
|
||||
|
||||
#[error("No mixnodes available on layer {layer}")]
|
||||
EmptyMixLayer { layer: MixLayer },
|
||||
|
||||
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
||||
UnevenLayerDistribution {
|
||||
layer: MixLayer,
|
||||
nodes: usize,
|
||||
lower_bound: usize,
|
||||
upper_bound: usize,
|
||||
total_nodes: usize,
|
||||
layer_distribution: Vec<(MixLayer, usize)>,
|
||||
},
|
||||
}
|
||||
+10
-33
@@ -14,45 +14,17 @@ use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
pub mod gateway;
|
||||
pub mod mix;
|
||||
pub mod random_route_provider;
|
||||
|
||||
#[cfg(feature = "provider-trait")]
|
||||
pub mod provider_trait;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum NymTopologyError {
|
||||
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
||||
EmptyNetworkTopology,
|
||||
|
||||
#[error("The provided network topology has no gateways available")]
|
||||
NoGatewaysAvailable,
|
||||
|
||||
#[error("The provided network topology has no mixnodes available")]
|
||||
NoMixnodesAvailable,
|
||||
|
||||
#[error("Gateway with identity key {identity_key} doesn't exist")]
|
||||
NonExistentGatewayError { identity_key: String },
|
||||
|
||||
#[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")]
|
||||
InvalidNumberOfHopsError { available: usize, requested: usize },
|
||||
|
||||
#[error("No mixnodes available on layer {layer}")]
|
||||
EmptyMixLayer { layer: MixLayer },
|
||||
|
||||
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
||||
UnevenLayerDistribution {
|
||||
layer: MixLayer,
|
||||
nodes: usize,
|
||||
lower_bound: usize,
|
||||
upper_bound: usize,
|
||||
total_nodes: usize,
|
||||
layer_distribution: Vec<(MixLayer, usize)>,
|
||||
},
|
||||
}
|
||||
pub use error::NymTopologyError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NetworkAddress {
|
||||
@@ -134,6 +106,12 @@ impl NymTopology {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::Node> {
|
||||
self.gateways
|
||||
.iter()
|
||||
.find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity)
|
||||
}
|
||||
|
||||
pub fn mixes(&self) -> &BTreeMap<MixLayer, Vec<mix::Node>> {
|
||||
&self.mixes
|
||||
}
|
||||
@@ -182,8 +160,7 @@ impl NymTopology {
|
||||
num_mix_hops: u8,
|
||||
) -> Result<Vec<SphinxNode>, NymTopologyError>
|
||||
where
|
||||
// I don't think there's a need for this RNG to be crypto-secure
|
||||
R: Rng + ?Sized,
|
||||
R: Rng + CryptoRng + ?Sized,
|
||||
{
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{NymTopology, NymTopologyError};
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_routing::SphinxRouteMaker;
|
||||
use nym_sphinx_types::Node;
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
pub struct NymTopologyRouteProvider<R> {
|
||||
rng: R,
|
||||
inner: NymTopology,
|
||||
}
|
||||
|
||||
impl<R> SphinxRouteMaker for NymTopologyRouteProvider<R>
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
type Error = NymTopologyError;
|
||||
|
||||
fn sphinx_route(
|
||||
&mut self,
|
||||
hops: u8,
|
||||
destination: &Recipient,
|
||||
) -> Result<Vec<Node>, NymTopologyError> {
|
||||
self.inner
|
||||
.random_route_to_gateway(&mut self.rng, hops, destination.gateway())
|
||||
}
|
||||
}
|
||||
Generated
+202
-12
@@ -536,7 +536,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
@@ -662,6 +662,101 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
@@ -949,6 +1044,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-vesting-integration-tests"
|
||||
version = "0.1.0"
|
||||
@@ -1045,13 +1146,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"ed25519-dalek",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"subtle-encoding",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
@@ -1121,6 +1222,36 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-name-service"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-name-service-common",
|
||||
"rand 0.8.5",
|
||||
"rstest",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-name-service-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "0.2.0"
|
||||
@@ -1152,6 +1283,7 @@ name = "nym-service-provider-directory-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1231,6 +1363,18 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.8.0"
|
||||
@@ -1338,6 +1482,17 @@ dependencies = [
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
@@ -1383,7 +1538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1430,6 +1585,32 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 1.0.109",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
@@ -1546,7 +1727,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.3",
|
||||
"syn 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1562,9 +1743,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.94"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -1579,7 +1760,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.3",
|
||||
"syn 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1605,6 +1786,15 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.1.0"
|
||||
@@ -1623,7 +1813,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"lioness",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"rand_distr",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
@@ -1679,9 +1869,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.3"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60"
|
||||
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1730,7 +1920,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.3",
|
||||
"syn 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -7,6 +7,7 @@ members = [
|
||||
"mixnet-vesting-integration-tests",
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"name-service",
|
||||
"service-provider-directory",
|
||||
"vesting",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "nym-name-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.4.0" }
|
||||
nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
serde = { version = "1.0.155", default-features = false, features = ["derive"] }
|
||||
thiserror = "1.0.39"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.40"
|
||||
cw-multi-test = { workspace = true }
|
||||
rand = "0.8.5"
|
||||
rstest = "0.17.0"
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
let mut config = Config::default();
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
// If we don't have access to git information, such as in a docs.rs build, don't error
|
||||
*config.git_mut().skip_if_error_mut() = true;
|
||||
}
|
||||
vergen(config).expect("failed to extract build metadata");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// We limit the these for simplicity and to avoid having to deal with paging.
|
||||
pub const MAX_NUMBER_OF_NAMES_PER_OWNER: u32 = 100;
|
||||
pub const MAX_NUMBER_OF_NAMES_FOR_ADDRESS: u32 = 100;
|
||||
|
||||
pub const NAME_DEFAULT_RETRIEVAL_LIMIT: u32 = 100;
|
||||
pub const NAME_MAX_RETRIEVAL_LIMIT: u32 = 150;
|
||||
|
||||
// Storage keys
|
||||
pub const CONFIG_KEY: &str = "config";
|
||||
pub const ADMIN_KEY: &str = "admin";
|
||||
pub const NAME_ID_COUNTER_KEY: &str = "nidc";
|
||||
|
||||
pub const NAMES_PK_NAMESPACE: &str = "nanames";
|
||||
pub const NAMES_OWNER_IDX_NAMESPACE: &str = "naowner";
|
||||
pub const NAMES_ADDRESS_IDX_NAMESPACE: &str = "naaddress";
|
||||
pub const NAMES_NAME_IDX_NAMESPACE: &str = "naname";
|
||||
@@ -0,0 +1,272 @@
|
||||
use crate::{
|
||||
error::{NameServiceError, Result},
|
||||
state::{self, Config},
|
||||
};
|
||||
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response};
|
||||
use nym_name_service_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
use semver::Version;
|
||||
|
||||
mod execute;
|
||||
mod query;
|
||||
|
||||
// version info for migration info
|
||||
const CONTRACT_NAME: &str = "crate:nym-name-service";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub fn instantiate(
|
||||
mut deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response> {
|
||||
state::set_admin(deps.branch(), info.sender.clone())?;
|
||||
|
||||
let config = Config {
|
||||
deposit_required: msg.deposit_required,
|
||||
};
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
state::save_config(deps.storage, &config)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("method", "instantiate")
|
||||
.add_attribute("admin", info.sender))
|
||||
}
|
||||
|
||||
pub fn migrate(
|
||||
deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
_msg: MigrateMsg,
|
||||
) -> Result<Response, NameServiceError> {
|
||||
// Note: don't remove this particular bit of code as we have to ALWAYS check whether we have to
|
||||
// update the stored version
|
||||
let version: Version = CONTRACT_VERSION.parse().map_err(|error: semver::Error| {
|
||||
NameServiceError::SemVerFailure {
|
||||
value: CONTRACT_VERSION.to_string(),
|
||||
error_message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let storage_version_raw = cw2::get_contract_version(deps.storage)?.version;
|
||||
let storage_version: Version =
|
||||
storage_version_raw
|
||||
.parse()
|
||||
.map_err(|error: semver::Error| NameServiceError::SemVerFailure {
|
||||
value: storage_version_raw,
|
||||
error_message: error.to_string(),
|
||||
})?;
|
||||
|
||||
if storage_version < version {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// If state structure changed in any contract version in the way migration is needed, it
|
||||
// should occur here, for example anything from `crate::queued_migrations::`
|
||||
}
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, NameServiceError> {
|
||||
match msg {
|
||||
ExecuteMsg::Register { name, address } => execute::register(deps, env, info, name, address),
|
||||
ExecuteMsg::DeleteId { name_id } => execute::delete_id(deps, info, name_id),
|
||||
ExecuteMsg::DeleteName { name } => execute::delete_name(deps, info, name),
|
||||
ExecuteMsg::UpdateDepositRequired { deposit_required } => {
|
||||
execute::update_deposit_required(deps, info, deposit_required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary> {
|
||||
let response = match msg {
|
||||
QueryMsg::NameId { name_id } => to_binary(&query::query_id(deps, name_id)?),
|
||||
QueryMsg::ByOwner { owner } => to_binary(&query::query_owner(deps, owner)?),
|
||||
QueryMsg::ByAddress { address } => to_binary(&query::query_address(deps, address)?),
|
||||
QueryMsg::ByName { name } => to_binary(&query::query_name(deps, name)?),
|
||||
QueryMsg::All { limit, start_after } => {
|
||||
to_binary(&query::query_all_paged(deps, limit, start_after)?)
|
||||
}
|
||||
QueryMsg::Config {} => to_binary(&query::query_config(deps)?),
|
||||
QueryMsg::GetContractVersion {} => to_binary(&query::query_contract_version()),
|
||||
QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?),
|
||||
};
|
||||
Ok(response?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_helpers::{
|
||||
assert::{assert_config, assert_empty, assert_name, assert_names, assert_not_found},
|
||||
fixture::name_fixture,
|
||||
helpers::{get_attribute, nyms},
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
testing::{mock_dependencies, mock_env, mock_info},
|
||||
Addr, Coin,
|
||||
};
|
||||
use nym_name_service_common::{msg::ExecuteMsg, NameEntry, NameId};
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
|
||||
#[test]
|
||||
fn instantiate_contract() {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg {
|
||||
deposit_required: Coin::new(100u128, DENOM),
|
||||
};
|
||||
let info = mock_info("creator", &[]);
|
||||
let admin = info.sender.clone();
|
||||
|
||||
// Instantiate contract
|
||||
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(res.messages.len(), 0);
|
||||
|
||||
// Check that it worked by querying the config, and checking that the list of names is
|
||||
// empty
|
||||
assert_config(deps.as_ref(), &admin, Coin::new(100u128, DENOM));
|
||||
assert_empty(deps.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_fails_incorrect_deposit() {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg::new(nyms(100));
|
||||
let info = mock_info("creator", &[]);
|
||||
let admin = info.sender.clone();
|
||||
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(res.messages.len(), 0);
|
||||
|
||||
// Register
|
||||
let msg: ExecuteMsg = name_fixture().into();
|
||||
let owner = name_fixture().owner.to_string();
|
||||
|
||||
assert_eq!(
|
||||
execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(&owner, &[nyms(99)]),
|
||||
msg.clone()
|
||||
)
|
||||
.unwrap_err(),
|
||||
NameServiceError::InsufficientDeposit {
|
||||
funds: 99u128.into(),
|
||||
deposit_required: 100u128.into(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(&owner, &[nyms(101)]),
|
||||
msg
|
||||
)
|
||||
.unwrap_err(),
|
||||
NameServiceError::TooLargeDeposit {
|
||||
funds: 101u128.into(),
|
||||
deposit_required: 100u128.into(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_config(deps.as_ref(), &admin, Coin::new(100, DENOM));
|
||||
assert_empty(deps.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_success() {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg::new(nyms(100));
|
||||
let info = mock_info("creator", &[]);
|
||||
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(res.messages.len(), 0);
|
||||
|
||||
// Register
|
||||
let msg: ExecuteMsg = name_fixture().into();
|
||||
let info = mock_info("steve", &[nyms(100)]);
|
||||
let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
// Check that the name has had name id assigned to it
|
||||
let expected_id = 1;
|
||||
let id: NameId = get_attribute(&res, "register", "name_id").parse().unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
assert_eq!(
|
||||
get_attribute(&res, "register", "name"),
|
||||
"my-service".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
get_attribute(&res, "register", "nym_address"),
|
||||
"client_id.client_key@gateway_id".to_string()
|
||||
);
|
||||
|
||||
// The expected registered name
|
||||
let expected_name = NameEntry {
|
||||
name_id: expected_id,
|
||||
name: name_fixture(),
|
||||
};
|
||||
assert_names(deps.as_ref(), &[expected_name.clone()]);
|
||||
assert_name(deps.as_ref(), &expected_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete() {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg::new(Coin::new(100, "unym"));
|
||||
let info = mock_info("creator", &[]);
|
||||
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(res.messages.len(), 0);
|
||||
|
||||
// Register
|
||||
let msg: ExecuteMsg = name_fixture().into();
|
||||
let info_steve = mock_info("steve", &[nyms(100)]);
|
||||
assert_eq!(info_steve.sender, name_fixture().owner);
|
||||
execute(deps.as_mut(), mock_env(), info_steve, msg).unwrap();
|
||||
|
||||
// The expected registerd name
|
||||
let expected_id = 1;
|
||||
let expected_name = NameEntry {
|
||||
name_id: expected_id,
|
||||
name: name_fixture(),
|
||||
};
|
||||
assert_names(deps.as_ref(), &[expected_name]);
|
||||
|
||||
// Removing someone else's name will fail
|
||||
let msg = ExecuteMsg::delete_id(expected_id);
|
||||
let info_timmy = mock_info("timmy", &[]);
|
||||
assert_eq!(
|
||||
execute(deps.as_mut(), mock_env(), info_timmy, msg).unwrap_err(),
|
||||
NameServiceError::Unauthorized {
|
||||
sender: Addr::unchecked("timmy")
|
||||
}
|
||||
);
|
||||
|
||||
// Removing an non-existent name will fail
|
||||
let msg = ExecuteMsg::delete_id(expected_id + 1);
|
||||
let info_owner = MessageInfo {
|
||||
sender: name_fixture().owner,
|
||||
funds: vec![],
|
||||
};
|
||||
assert_eq!(
|
||||
execute(deps.as_mut(), mock_env(), info_owner.clone(), msg).unwrap_err(),
|
||||
NameServiceError::NotFound {
|
||||
name_id: expected_id + 1
|
||||
}
|
||||
);
|
||||
|
||||
// Remove as correct owner succeeds
|
||||
let msg = ExecuteMsg::delete_id(expected_id);
|
||||
let res = execute(deps.as_mut(), mock_env(), info_owner, msg).unwrap();
|
||||
assert_eq!(
|
||||
get_attribute(&res, "delete_id", "name_id"),
|
||||
expected_id.to_string()
|
||||
);
|
||||
assert_names(deps.as_ref(), &[]);
|
||||
assert_not_found(deps.as_ref(), expected_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::{
|
||||
constants::{MAX_NUMBER_OF_NAMES_FOR_ADDRESS, MAX_NUMBER_OF_NAMES_PER_OWNER},
|
||||
error::{NameServiceError, Result},
|
||||
state,
|
||||
};
|
||||
use cosmwasm_std::{Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Response, Uint128};
|
||||
use nym_name_service_common::{
|
||||
events::{
|
||||
new_delete_id_event, new_delete_name_event, new_register_event,
|
||||
new_update_deposit_required_event,
|
||||
},
|
||||
Address, NameId, NymName, RegisteredName,
|
||||
};
|
||||
|
||||
use super::query;
|
||||
|
||||
fn ensure_correct_deposit(will_deposit: Uint128, deposit_required: Uint128) -> Result<()> {
|
||||
match will_deposit.cmp(&deposit_required) {
|
||||
std::cmp::Ordering::Less => Err(NameServiceError::InsufficientDeposit {
|
||||
funds: will_deposit,
|
||||
deposit_required,
|
||||
}),
|
||||
std::cmp::Ordering::Equal => Ok(()),
|
||||
std::cmp::Ordering::Greater => Err(NameServiceError::TooLargeDeposit {
|
||||
funds: will_deposit,
|
||||
deposit_required,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_max_names_per_owner(deps: Deps, owner: Addr) -> Result<()> {
|
||||
let current_entries = query::query_owner(deps, owner.to_string())?;
|
||||
if current_entries.names.len() < MAX_NUMBER_OF_NAMES_PER_OWNER as usize {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NameServiceError::ReachedMaxNamesForOwner {
|
||||
max_names: MAX_NUMBER_OF_NAMES_PER_OWNER,
|
||||
owner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_max_names_per_address(deps: Deps, address: Address) -> Result<()> {
|
||||
let current_entries = query::query_address(deps, address.clone())?;
|
||||
if current_entries.names.len() < MAX_NUMBER_OF_NAMES_FOR_ADDRESS as usize {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NameServiceError::ReachedMaxNamesForAddress {
|
||||
max_names: MAX_NUMBER_OF_NAMES_FOR_ADDRESS,
|
||||
address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_name_exists(deps: Deps, name_id: NameId) -> Result<()> {
|
||||
if state::names::has_name_id(deps.storage, name_id) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NameServiceError::NotFound { name_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_name_not_exists(deps: Deps, name: &NymName) -> Result<()> {
|
||||
if state::names::has_name(deps.storage, name) {
|
||||
println!("name already exists");
|
||||
Err(NameServiceError::NameAlreadyRegistered { name: name.clone() })
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_sender_authorized(info: MessageInfo, names: &RegisteredName) -> Result<()> {
|
||||
if info.sender == names.owner {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NameServiceError::Unauthorized {
|
||||
sender: info.sender,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn return_deposit(name_to_delete: &RegisteredName) -> BankMsg {
|
||||
BankMsg::Send {
|
||||
to_address: name_to_delete.owner.to_string(),
|
||||
amount: vec![name_to_delete.deposit.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new name. It will be assigned a new name id.
|
||||
pub fn register(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
name: NymName,
|
||||
address: Address,
|
||||
) -> Result<Response> {
|
||||
ensure_name_not_exists(deps.as_ref(), &name)?;
|
||||
ensure_max_names_per_owner(deps.as_ref(), info.sender.clone())?;
|
||||
ensure_max_names_per_address(deps.as_ref(), address.clone())?;
|
||||
|
||||
let deposit_required = state::deposit_required(deps.storage)?;
|
||||
let denom = deposit_required.denom.clone();
|
||||
let will_deposit = cw_utils::must_pay(&info, &denom)
|
||||
.map_err(|err| NameServiceError::DepositRequired { source: err })?;
|
||||
ensure_correct_deposit(will_deposit, deposit_required.amount)?;
|
||||
|
||||
let new_name = RegisteredName {
|
||||
address,
|
||||
name,
|
||||
owner: info.sender,
|
||||
block_height: env.block.height,
|
||||
deposit: Coin::new(will_deposit.u128(), denom),
|
||||
};
|
||||
let name_id = state::names::save(deps.storage, &new_name)?;
|
||||
|
||||
Ok(Response::new().add_event(new_register_event(name_id, new_name)))
|
||||
}
|
||||
|
||||
/// Delete an exsisting name.
|
||||
pub fn delete_id(deps: DepsMut, info: MessageInfo, name_id: NameId) -> Result<Response> {
|
||||
ensure_name_exists(deps.as_ref(), name_id)?;
|
||||
let name_to_delete = state::names::load_id(deps.storage, name_id)?;
|
||||
ensure_sender_authorized(info, &name_to_delete)?;
|
||||
|
||||
state::names::remove_id(deps.storage, name_id)?;
|
||||
let return_deposit_msg = return_deposit(&name_to_delete);
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(return_deposit_msg)
|
||||
.add_event(new_delete_id_event(name_id, name_to_delete)))
|
||||
}
|
||||
|
||||
/// Delete an existing name by name.
|
||||
pub(crate) fn delete_name(deps: DepsMut, info: MessageInfo, name: NymName) -> Result<Response> {
|
||||
let name_to_delete = query::query_name(deps.as_ref(), name)?;
|
||||
ensure_sender_authorized(info, &name_to_delete.name)?;
|
||||
|
||||
state::names::remove_id(deps.storage, name_to_delete.name_id)?;
|
||||
let return_deposit_msg = return_deposit(&name_to_delete.name);
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(return_deposit_msg)
|
||||
.add_event(new_delete_name_event(
|
||||
name_to_delete.name_id,
|
||||
name_to_delete.name,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Update the deposit required to register new names
|
||||
pub(crate) fn update_deposit_required(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
deposit_required: Coin,
|
||||
) -> Result<Response> {
|
||||
state::assert_admin(deps.as_ref(), &info.sender)?;
|
||||
|
||||
let mut config = state::load_config(deps.storage)?;
|
||||
config.deposit_required = deposit_required.clone();
|
||||
state::save_config(deps.storage, &config)?;
|
||||
|
||||
Ok(Response::new().add_event(new_update_deposit_required_event(deposit_required)))
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use cosmwasm_std::Deps;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_name_service_common::{
|
||||
response::{ConfigResponse, NamesListResponse, PagedNamesListResponse},
|
||||
Address, NameEntry, NameId, NymName,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Result,
|
||||
state::{self, names::PagedLoad},
|
||||
};
|
||||
|
||||
pub fn query_id(deps: Deps, name_id: NameId) -> Result<NameEntry> {
|
||||
let name = state::names::load_id(deps.storage, name_id)?;
|
||||
Ok(NameEntry { name_id, name })
|
||||
}
|
||||
|
||||
pub fn query_owner(deps: Deps, owner: String) -> Result<NamesListResponse> {
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
let names = state::names::load_owner(deps.storage, owner)?;
|
||||
Ok(NamesListResponse::new(names))
|
||||
}
|
||||
|
||||
pub fn query_address(deps: Deps, address: Address) -> Result<NamesListResponse> {
|
||||
let names = state::names::load_address(deps.storage, &address)?;
|
||||
Ok(NamesListResponse::new(names))
|
||||
}
|
||||
|
||||
pub fn query_name(deps: Deps, name: NymName) -> Result<NameEntry> {
|
||||
state::names::load_name_entry(deps.storage, &name)
|
||||
.map(|(name_id, name)| NameEntry::new(name_id, name))
|
||||
}
|
||||
|
||||
pub fn query_all_paged(
|
||||
deps: Deps,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<NameId>,
|
||||
) -> Result<PagedNamesListResponse> {
|
||||
let PagedLoad {
|
||||
names,
|
||||
limit,
|
||||
start_next_after,
|
||||
} = state::names::load_all_paged(deps.storage, limit, start_after)?;
|
||||
Ok(PagedNamesListResponse::new(names, limit, start_next_after))
|
||||
}
|
||||
|
||||
pub fn query_config(deps: Deps) -> Result<ConfigResponse> {
|
||||
let config = state::load_config(deps.storage)?;
|
||||
Ok(config.into())
|
||||
}
|
||||
|
||||
pub fn query_contract_version() -> ContractBuildInformation {
|
||||
// as per docs
|
||||
// env! macro will expand to the value of the named environment variable at
|
||||
// compile time, yielding an expression of type `&'static str`
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_branch: option_env!("VERGEN_GIT_BRANCH")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use cosmwasm_std::{Addr, StdError};
|
||||
use cw_controllers::AdminError;
|
||||
use nym_name_service_common::{Address, NameId, NymName};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NameServiceError {
|
||||
#[error("{0}")]
|
||||
Std(#[from] StdError),
|
||||
|
||||
#[error("{0}")]
|
||||
AdminError(#[from] AdminError),
|
||||
|
||||
#[error("name id entry not found: {name_id}")]
|
||||
NotFound { name_id: NameId },
|
||||
|
||||
#[error("name entry not found: {name}")]
|
||||
NameNotFound { name: NymName },
|
||||
|
||||
#[error("{sender} is not registrator of name")]
|
||||
Unauthorized { sender: Addr },
|
||||
|
||||
#[error("deposit required to register a name")]
|
||||
DepositRequired { source: cw_utils::PaymentError },
|
||||
|
||||
#[error("insufficiant deposit: {funds}, required: {deposit_required}")]
|
||||
InsufficientDeposit {
|
||||
funds: cosmwasm_std::Uint128,
|
||||
deposit_required: cosmwasm_std::Uint128,
|
||||
},
|
||||
|
||||
#[error("deposit too large: {funds}, required: {deposit_required}")]
|
||||
TooLargeDeposit {
|
||||
funds: cosmwasm_std::Uint128,
|
||||
deposit_required: cosmwasm_std::Uint128,
|
||||
},
|
||||
|
||||
#[error("reached the max number of names ({max_names}) for owner {owner}")]
|
||||
ReachedMaxNamesForOwner { max_names: u32, owner: Addr },
|
||||
|
||||
#[error("reached the max number of names ({max_names}) for address {0}", address.to_string())]
|
||||
ReachedMaxNamesForAddress { max_names: u32, address: Address },
|
||||
|
||||
#[error("failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||
SemVerFailure {
|
||||
value: String,
|
||||
error_message: String,
|
||||
},
|
||||
|
||||
#[error("duplicate entries detected for name: {name}")]
|
||||
DuplicateNames { name: NymName },
|
||||
|
||||
#[error("name already registered: {name}")]
|
||||
NameAlreadyRegistered { name: NymName },
|
||||
}
|
||||
|
||||
pub(crate) type Result<T, E = NameServiceError> = std::result::Result<T, E>;
|
||||
@@ -0,0 +1,445 @@
|
||||
//! Integration tests using cw-multi-test.
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_name_service_common::{
|
||||
response::{ConfigResponse, PagedNamesListResponse},
|
||||
Address, NameEntry, NymName, RegisteredName,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::NAME_DEFAULT_RETRIEVAL_LIMIT,
|
||||
error::NameServiceError,
|
||||
test_helpers::{fixture::name_entry, helpers::nyms, test_setup::TestSetup},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn instantiate_contract() {
|
||||
TestSetup::new();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_config() {
|
||||
assert_eq!(
|
||||
TestSetup::new().query_config(),
|
||||
ConfigResponse {
|
||||
deposit_required: nyms(100),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_and_query_name() {
|
||||
let mut setup = TestSetup::new();
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: None,
|
||||
}
|
||||
);
|
||||
|
||||
// Register a first name
|
||||
let owner = Addr::unchecked("owner");
|
||||
let name = NymName::new("steves-server").unwrap();
|
||||
let nym_address = Address::new("nym-address");
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert_eq!(setup.balance(&owner), nyms(250));
|
||||
setup.register(name.clone(), nym_address.clone(), owner.clone());
|
||||
|
||||
// Deposit is deposited to contract and deducted from owners's balance
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(setup.balance(&owner), nyms(150));
|
||||
|
||||
// We can query the full name list
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![NameEntry {
|
||||
name_id: 1,
|
||||
name: RegisteredName {
|
||||
address: nym_address.clone(),
|
||||
name: name.clone(),
|
||||
owner: owner.clone(),
|
||||
block_height: 12345,
|
||||
deposit: nyms(100),
|
||||
},
|
||||
}],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(1),
|
||||
}
|
||||
);
|
||||
|
||||
// ... and we can query by id
|
||||
assert_eq!(
|
||||
setup.query_id(1),
|
||||
NameEntry {
|
||||
name_id: 1,
|
||||
name: RegisteredName {
|
||||
address: nym_address.clone(),
|
||||
name: name.clone(),
|
||||
owner: owner.clone(),
|
||||
block_height: 12345,
|
||||
deposit: nyms(100),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Register a second name
|
||||
let owner2 = Addr::unchecked("owner2");
|
||||
let name2 = NymName::new("another_server").unwrap();
|
||||
let nym_address2 = Address::new("nymAddress2");
|
||||
setup.register(name2.clone(), nym_address2.clone(), owner2.clone());
|
||||
|
||||
assert_eq!(setup.contract_balance(), nyms(200));
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(1, name, nym_address, owner),
|
||||
name_entry(2, name2, nym_address2, owner2)
|
||||
],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(2),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_register_a_name_without_funds() {
|
||||
let mut setup = TestSetup::new();
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert_eq!(setup.balance("owner"), nyms(250));
|
||||
setup.register(
|
||||
NymName::new("my_name").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(setup.balance("owner"), nyms(150));
|
||||
setup.register(
|
||||
NymName::new("my_name2").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
assert_eq!(setup.contract_balance(), nyms(200));
|
||||
assert_eq!(setup.balance("owner"), nyms(50));
|
||||
let res = setup
|
||||
.try_register(
|
||||
NymName::new("my_name3").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
res.downcast::<cosmwasm_std::StdError>().unwrap(),
|
||||
cosmwasm_std::StdError::Overflow {
|
||||
source: cosmwasm_std::OverflowError::new(
|
||||
cosmwasm_std::OverflowOperation::Sub,
|
||||
"50",
|
||||
"100"
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_name() {
|
||||
let mut setup = TestSetup::new();
|
||||
setup.register(
|
||||
NymName::new("my_name").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(setup.balance("owner"), nyms(150));
|
||||
assert!(!setup.query_all().names.is_empty());
|
||||
setup.delete(1, Addr::unchecked("owner"));
|
||||
|
||||
// Deleting the name returns the deposit to the owner
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert_eq!(setup.balance("owner"), nyms(250));
|
||||
assert!(setup.query_all().names.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_owner_can_delete_name() {
|
||||
let mut setup = TestSetup::new();
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
setup.register(
|
||||
NymName::new("name").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert!(!setup.query_all().names.is_empty());
|
||||
|
||||
let delete_resp = setup
|
||||
.try_delete(1, Addr::unchecked("not_owner"))
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(
|
||||
delete_resp.downcast::<NameServiceError>().unwrap(),
|
||||
NameServiceError::Unauthorized {
|
||||
sender: Addr::unchecked("not_owner")
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_delete_name_that_does_not_exist() {
|
||||
let mut setup = TestSetup::new();
|
||||
setup.register(
|
||||
NymName::new("foo").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert!(!setup.query_all().names.is_empty());
|
||||
|
||||
let delete_resp = setup.try_delete(0, Addr::unchecked("owner")).unwrap_err();
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(
|
||||
delete_resp.downcast::<NameServiceError>().unwrap(),
|
||||
NameServiceError::NotFound { name_id: 0 }
|
||||
);
|
||||
|
||||
let delete_resp = setup.try_delete(2, Addr::unchecked("owner")).unwrap_err();
|
||||
assert_eq!(setup.contract_balance(), nyms(100));
|
||||
assert_eq!(
|
||||
delete_resp.downcast::<NameServiceError>().unwrap(),
|
||||
NameServiceError::NotFound { name_id: 2 }
|
||||
);
|
||||
|
||||
assert!(!setup.query_all().names.is_empty());
|
||||
setup.delete(1, Addr::unchecked("owner"));
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert!(setup.query_all().names.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_register_the_same_name_multiple_times() {
|
||||
let mut setup = TestSetup::new();
|
||||
|
||||
setup.register(
|
||||
NymName::new("name").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
);
|
||||
let resp = setup
|
||||
.try_register(
|
||||
NymName::new("name").unwrap(),
|
||||
Address::new("nymAddress"),
|
||||
Addr::unchecked("owner"),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
resp.downcast::<NameServiceError>().unwrap(),
|
||||
NameServiceError::NameAlreadyRegistered {
|
||||
name: NymName::new("name").unwrap()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_register_multiple_names_for_the_same_nym_address() {
|
||||
let mut setup = TestSetup::new();
|
||||
let name1 = NymName::new("name1").unwrap();
|
||||
let name2 = NymName::new("name2").unwrap();
|
||||
let address = Address::new("nymaddress");
|
||||
let owner = Addr::unchecked("owner");
|
||||
|
||||
setup.register(name1.clone(), address.clone(), owner.clone());
|
||||
setup.register(name2.clone(), address.clone(), owner.clone());
|
||||
|
||||
assert_eq!(
|
||||
setup.query_all().names,
|
||||
vec![
|
||||
name_entry(1, name1, address.clone(), owner.clone()),
|
||||
name_entry(2, name2, address, owner)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_multiple_names_and_deleting_by_name() {
|
||||
let mut setup = TestSetup::new();
|
||||
let owner1 = Addr::unchecked("wealthy_owner_1");
|
||||
let owner2 = Addr::unchecked("wealthy_owner_2");
|
||||
let nym_address1 = Address::new("nymaddress1");
|
||||
let nym_address2 = Address::new("nymaddress2");
|
||||
let name1 = NymName::new("name1").unwrap();
|
||||
let name2 = NymName::new("name2").unwrap();
|
||||
let name3 = NymName::new("name3").unwrap();
|
||||
let name4 = NymName::new("name4").unwrap();
|
||||
let name5 = NymName::new("name5").unwrap();
|
||||
|
||||
// We register the same address three times, but with different owners
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert_eq!(setup.balance(&owner1), nyms(1000));
|
||||
setup.register(name1.clone(), nym_address1.clone(), owner1.clone());
|
||||
setup.register(name2.clone(), nym_address1.clone(), owner1.clone());
|
||||
setup.register(name3.clone(), nym_address2.clone(), owner1.clone());
|
||||
setup.register(name4.clone(), nym_address1.clone(), owner2.clone());
|
||||
setup.register(name5.clone(), nym_address2.clone(), owner2.clone());
|
||||
|
||||
assert_eq!(setup.contract_balance(), nyms(500));
|
||||
assert_eq!(setup.balance(&owner1), nyms(700));
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(1, name1.clone(), nym_address1.clone(), owner1.clone()),
|
||||
name_entry(2, name2.clone(), nym_address1.clone(), owner1.clone()),
|
||||
name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()),
|
||||
name_entry(4, name4.clone(), nym_address1.clone(), owner2.clone()),
|
||||
name_entry(5, name5.clone(), nym_address2.clone(), owner2.clone()),
|
||||
],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(5),
|
||||
}
|
||||
);
|
||||
|
||||
setup.delete_name(name1, owner1.clone());
|
||||
|
||||
assert_eq!(setup.contract_balance(), nyms(400));
|
||||
assert_eq!(setup.balance(&owner1), nyms(800));
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(2, name2, nym_address1.clone(), owner1.clone()),
|
||||
name_entry(3, name3, nym_address2.clone(), owner1),
|
||||
name_entry(4, name4, nym_address1, owner2.clone()),
|
||||
name_entry(5, name5, nym_address2, owner2),
|
||||
],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(5),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_paging() {
|
||||
let mut setup = TestSetup::new();
|
||||
let owner1 = Addr::unchecked("wealthy_owner_1");
|
||||
let owner2 = Addr::unchecked("wealthy_owner_2");
|
||||
let nym_address1 = Address::new("nymAddress1");
|
||||
let nym_address2 = Address::new("nymAddress2");
|
||||
let name1 = NymName::new("name1").unwrap();
|
||||
let name2 = NymName::new("name2").unwrap();
|
||||
let name3 = NymName::new("name3").unwrap();
|
||||
let name4 = NymName::new("name4").unwrap();
|
||||
let name5 = NymName::new("name5").unwrap();
|
||||
|
||||
// We register the same address three times, but with different owners
|
||||
assert_eq!(setup.contract_balance(), nyms(0));
|
||||
assert_eq!(setup.balance(&owner1), nyms(1000));
|
||||
setup.register(name1.clone(), nym_address1.clone(), owner1.clone());
|
||||
setup.register(name2.clone(), nym_address1.clone(), owner1.clone());
|
||||
setup.register(name3.clone(), nym_address2.clone(), owner1.clone());
|
||||
setup.register(name4.clone(), nym_address1.clone(), owner2.clone());
|
||||
setup.register(name5.clone(), nym_address2.clone(), owner2.clone());
|
||||
|
||||
assert_eq!(setup.contract_balance(), nyms(500));
|
||||
assert_eq!(setup.balance(&owner1), nyms(700));
|
||||
assert_eq!(
|
||||
setup.query_all(),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(1, name1.clone(), nym_address1.clone(), owner1.clone()),
|
||||
name_entry(2, name2.clone(), nym_address1.clone(), owner1.clone()),
|
||||
name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()),
|
||||
name_entry(4, name4.clone(), nym_address1.clone(), owner2.clone()),
|
||||
name_entry(5, name5, nym_address2.clone(), owner2.clone()),
|
||||
],
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(5),
|
||||
}
|
||||
);
|
||||
|
||||
setup.delete_name(name1, owner1.clone());
|
||||
|
||||
assert_eq!(
|
||||
setup.query_all_with_limit(Some(2), None),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(2, name2, nym_address1.clone(), owner1.clone()),
|
||||
name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()),
|
||||
],
|
||||
per_page: 2,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
setup.query_all_with_limit(Some(2), Some(2)),
|
||||
PagedNamesListResponse {
|
||||
names: vec![
|
||||
name_entry(3, name3, nym_address2, owner1),
|
||||
name_entry(4, name4, nym_address1, owner2),
|
||||
],
|
||||
per_page: 2,
|
||||
start_next_after: Some(4),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_id_is_not_resused_when_deleting_and_then_adding_a_new_names() {
|
||||
let mut setup = TestSetup::new();
|
||||
setup.register(
|
||||
NymName::new("myname1").unwrap(),
|
||||
Address::new("nymAddress1"),
|
||||
Addr::unchecked("owner1"),
|
||||
);
|
||||
setup.register(
|
||||
NymName::new("myname2").unwrap(),
|
||||
Address::new("nymAddress2"),
|
||||
Addr::unchecked("owner2"),
|
||||
);
|
||||
setup.register(
|
||||
NymName::new("myname3").unwrap(),
|
||||
Address::new("nymAddress3"),
|
||||
Addr::unchecked("owner3"),
|
||||
);
|
||||
|
||||
setup.delete(1, Addr::unchecked("owner1"));
|
||||
setup.delete(3, Addr::unchecked("owner3"));
|
||||
|
||||
assert_eq!(
|
||||
setup.query_all().names,
|
||||
vec![name_entry(
|
||||
2,
|
||||
NymName::new("myname2").unwrap(),
|
||||
Address::new("nymAddress2"),
|
||||
Addr::unchecked("owner2")
|
||||
)]
|
||||
);
|
||||
|
||||
setup.register(
|
||||
NymName::new("myname4").unwrap(),
|
||||
Address::new("nymAddress4"),
|
||||
Addr::unchecked("owner4"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
setup.query_all().names,
|
||||
vec![
|
||||
name_entry(
|
||||
2,
|
||||
NymName::new("myname2").unwrap(),
|
||||
Address::new("nymAddress2"),
|
||||
Addr::unchecked("owner2")
|
||||
),
|
||||
name_entry(
|
||||
4,
|
||||
NymName::new("myname4").unwrap(),
|
||||
Address::new("nymAddress4"),
|
||||
Addr::unchecked("owner4")
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//! The nym name service contract is for users to register names for the nym addresses.
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::error::Result;
|
||||
use nym_name_service_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
#[cfg(not(feature = "library"))]
|
||||
use cosmwasm_std::entry_point;
|
||||
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response};
|
||||
use error::NameServiceError;
|
||||
|
||||
mod contract;
|
||||
mod error;
|
||||
mod state;
|
||||
|
||||
pub mod constants;
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
#[cfg(test)]
|
||||
mod test_helpers;
|
||||
|
||||
/// Contract entry point for instantiation.
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response> {
|
||||
contract::instantiate(deps, env, info, msg)
|
||||
}
|
||||
|
||||
/// Contract entry point for migrations.
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result<Response, NameServiceError> {
|
||||
contract::migrate(deps, env, msg)
|
||||
}
|
||||
|
||||
/// Contract entry point for execution
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, NameServiceError> {
|
||||
contract::execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
/// Contract entry point for queries
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<Binary> {
|
||||
contract::query(deps, env, msg)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut};
|
||||
use cw_controllers::Admin;
|
||||
|
||||
use crate::{constants::ADMIN_KEY, error::Result};
|
||||
|
||||
const ADMIN: Admin = Admin::new(ADMIN_KEY);
|
||||
|
||||
pub(crate) fn set_admin(deps: DepsMut<'_>, admin: Addr) -> Result<()> {
|
||||
Ok(ADMIN.set(deps, Some(admin))?)
|
||||
}
|
||||
|
||||
pub(crate) fn assert_admin(deps: Deps, sender: &Addr) -> Result<()> {
|
||||
Ok(ADMIN.assert_admin(deps, sender)?)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use cosmwasm_std::{Coin, Storage};
|
||||
use cw_storage_plus::Item;
|
||||
use nym_name_service_common::response::ConfigResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{constants::CONFIG_KEY, error::Result};
|
||||
|
||||
const CONFIG: Item<Config> = Item::new(CONFIG_KEY);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub(crate) struct Config {
|
||||
pub deposit_required: Coin,
|
||||
}
|
||||
|
||||
impl From<Config> for ConfigResponse {
|
||||
fn from(config: Config) -> Self {
|
||||
ConfigResponse {
|
||||
deposit_required: config.deposit_required,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn save_config(store: &mut dyn Storage, config: &Config) -> Result<()> {
|
||||
Ok(CONFIG.save(store, config)?)
|
||||
}
|
||||
|
||||
pub(crate) fn load_config(store: &dyn Storage) -> Result<Config> {
|
||||
Ok(CONFIG.load(store)?)
|
||||
}
|
||||
|
||||
/// Return the deposit required to register a name.
|
||||
pub(crate) fn deposit_required(store: &dyn Storage) -> Result<Coin> {
|
||||
Ok(CONFIG.load(store).map(|config| config.deposit_required)?)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod admin;
|
||||
pub mod config;
|
||||
pub mod name_id_counter;
|
||||
pub mod names;
|
||||
|
||||
pub(crate) use admin::{assert_admin, set_admin};
|
||||
pub(crate) use config::{deposit_required, load_config, save_config, Config};
|
||||
pub(crate) use name_id_counter::next_name_id_counter;
|
||||
@@ -0,0 +1,83 @@
|
||||
use cosmwasm_std::Storage;
|
||||
use cw_storage_plus::Item;
|
||||
use nym_name_service_common::NameId;
|
||||
|
||||
use crate::{constants::NAME_ID_COUNTER_KEY, error::Result};
|
||||
|
||||
const NAME_ID_COUNTER: Item<NameId> = Item::new(NAME_ID_COUNTER_KEY);
|
||||
|
||||
/// Generate the next name id, store it and return it
|
||||
pub(crate) fn next_name_id_counter(store: &mut dyn Storage) -> Result<NameId> {
|
||||
// The first id is 1.
|
||||
let id = NAME_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1;
|
||||
NAME_ID_COUNTER.save(store, &id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use nym_name_service_common::NameEntry;
|
||||
|
||||
use crate::test_helpers::{
|
||||
assert::assert_names,
|
||||
fixture::name_fixture_name,
|
||||
helpers::{delete_name_id, instantiate_test_contract, register_name},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn get_next_name_id() {
|
||||
let mut deps = instantiate_test_contract();
|
||||
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("foo")), 1);
|
||||
assert_names(
|
||||
deps.as_ref(),
|
||||
&[NameEntry::new(1, name_fixture_name("foo"))],
|
||||
);
|
||||
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("bar")), 2);
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("baz")), 3);
|
||||
assert_names(
|
||||
deps.as_ref(),
|
||||
&[
|
||||
NameEntry::new(1, name_fixture_name("foo")),
|
||||
NameEntry::new(2, name_fixture_name("bar")),
|
||||
NameEntry::new(3, name_fixture_name("baz")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_name_id_is_not_reused() {
|
||||
let mut deps = instantiate_test_contract();
|
||||
|
||||
// Register two names
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("one")), 1);
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("two")), 2);
|
||||
assert_names(
|
||||
deps.as_ref(),
|
||||
&[
|
||||
NameEntry::new(1, name_fixture_name("one")),
|
||||
NameEntry::new(2, name_fixture_name("two")),
|
||||
],
|
||||
);
|
||||
|
||||
// Delete the last entry
|
||||
delete_name_id(deps.as_mut(), 2, "steve");
|
||||
assert_names(
|
||||
deps.as_ref(),
|
||||
&[NameEntry::new(1, name_fixture_name("one"))],
|
||||
);
|
||||
|
||||
// Create a third entry. The index should not reuse the previous entry that we just
|
||||
// deleted.
|
||||
assert_eq!(register_name(deps.as_mut(), &name_fixture_name("two")), 3);
|
||||
assert_names(
|
||||
deps.as_ref(),
|
||||
&[
|
||||
NameEntry::new(1, name_fixture_name("one")),
|
||||
NameEntry::new(3, name_fixture_name("two")),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
use cosmwasm_std::{Addr, Order, StdError, StdResult, Storage};
|
||||
use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex};
|
||||
use nym_name_service_common::{Address, NameId, NymName, RegisteredName};
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
MAX_NUMBER_OF_NAMES_FOR_ADDRESS, MAX_NUMBER_OF_NAMES_PER_OWNER,
|
||||
NAMES_ADDRESS_IDX_NAMESPACE, NAMES_NAME_IDX_NAMESPACE, NAMES_OWNER_IDX_NAMESPACE,
|
||||
NAMES_PK_NAMESPACE, NAME_DEFAULT_RETRIEVAL_LIMIT, NAME_MAX_RETRIEVAL_LIMIT,
|
||||
},
|
||||
error::{NameServiceError, Result},
|
||||
};
|
||||
|
||||
struct NameIndex<'a> {
|
||||
// A name can only point to a single address
|
||||
pub(crate) name: UniqueIndex<'a, String, RegisteredName, NameId>,
|
||||
// An addresses can be pointed to by multiple names.
|
||||
pub(crate) address: MultiIndex<'a, String, RegisteredName, NameId>,
|
||||
pub(crate) owner: MultiIndex<'a, Addr, RegisteredName, NameId>,
|
||||
}
|
||||
|
||||
impl<'a> IndexList<RegisteredName> for NameIndex<'a> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<RegisteredName>> + '_> {
|
||||
let v: Vec<&dyn Index<RegisteredName>> = vec![&self.name, &self.address, &self.owner];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
fn names<'a>() -> IndexedMap<'a, NameId, RegisteredName, NameIndex<'a>> {
|
||||
let indexes = NameIndex {
|
||||
name: UniqueIndex::new(|d| d.name.to_string(), NAMES_NAME_IDX_NAMESPACE),
|
||||
address: MultiIndex::new(
|
||||
|d| d.address.to_string(),
|
||||
NAMES_PK_NAMESPACE,
|
||||
NAMES_ADDRESS_IDX_NAMESPACE,
|
||||
),
|
||||
owner: MultiIndex::new(
|
||||
|d| d.owner.clone(),
|
||||
NAMES_PK_NAMESPACE,
|
||||
NAMES_OWNER_IDX_NAMESPACE,
|
||||
),
|
||||
};
|
||||
IndexedMap::new(NAMES_PK_NAMESPACE, indexes)
|
||||
}
|
||||
|
||||
pub fn save(store: &mut dyn Storage, new_name: &RegisteredName) -> Result<NameId> {
|
||||
let name_id = super::next_name_id_counter(store)?;
|
||||
names().save(store, name_id, new_name)?;
|
||||
Ok(name_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn save_all(state: &mut dyn Storage, names: &[RegisteredName]) -> Result<Vec<NameId>> {
|
||||
let mut ids = vec![];
|
||||
for name in names {
|
||||
ids.push(save(state, name)?);
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
pub fn has_name_id(store: &dyn Storage, name_id: NameId) -> bool {
|
||||
names().has(store, name_id)
|
||||
}
|
||||
|
||||
pub fn has_name(store: &dyn Storage, name: &NymName) -> bool {
|
||||
load_name(store, name).is_ok()
|
||||
}
|
||||
|
||||
// Get the (key, name) entry for a given name
|
||||
pub fn load_name_entry(store: &dyn Storage, name: &NymName) -> Result<(NameId, RegisteredName)> {
|
||||
names()
|
||||
.idx
|
||||
.name
|
||||
.range(store, None, None, Order::Ascending)
|
||||
.find(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
&entry.1.name == name
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.ok_or(NameServiceError::NameNotFound { name: name.clone() })?
|
||||
.map_err(NameServiceError::from)
|
||||
}
|
||||
|
||||
pub fn load_id(store: &dyn Storage, name_id: NameId) -> Result<RegisteredName> {
|
||||
names().load(store, name_id).map_err(|err| match err {
|
||||
StdError::NotFound { .. } => NameServiceError::NotFound { name_id },
|
||||
err => err.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_name(store: &dyn Storage, name: &NymName) -> Result<RegisteredName> {
|
||||
names()
|
||||
.idx
|
||||
.name
|
||||
.item(store, name.to_string())?
|
||||
.map(|record| record.1)
|
||||
.ok_or(NameServiceError::NameNotFound { name: name.clone() })
|
||||
}
|
||||
|
||||
pub fn load_address(
|
||||
store: &dyn Storage,
|
||||
address: &Address,
|
||||
) -> Result<Vec<(NameId, RegisteredName)>> {
|
||||
names()
|
||||
.idx
|
||||
.address
|
||||
.prefix(address.to_string())
|
||||
.range(store, None, None, Order::Ascending)
|
||||
.take(MAX_NUMBER_OF_NAMES_FOR_ADDRESS as usize)
|
||||
.collect::<StdResult<Vec<_>>>()
|
||||
.map_err(NameServiceError::from)
|
||||
}
|
||||
|
||||
pub fn load_owner(store: &dyn Storage, owner: Addr) -> Result<Vec<(NameId, RegisteredName)>> {
|
||||
names()
|
||||
.idx
|
||||
.owner
|
||||
.prefix(owner)
|
||||
.range(store, None, None, Order::Ascending)
|
||||
.take(MAX_NUMBER_OF_NAMES_PER_OWNER as usize)
|
||||
.collect::<StdResult<Vec<_>>>()
|
||||
.map_err(NameServiceError::from)
|
||||
}
|
||||
|
||||
pub fn remove_id(store: &mut dyn Storage, name_id: NameId) -> Result<()> {
|
||||
Ok(names().remove(store, name_id)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn remove_name(store: &mut dyn Storage, name: NymName) -> Result<NameId> {
|
||||
let name_info = load_name_entry(store, &name)?;
|
||||
remove_id(store, name_info.0)?;
|
||||
Ok(name_info.0)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PagedLoad {
|
||||
pub names: Vec<(NameId, RegisteredName)>,
|
||||
pub limit: usize,
|
||||
pub start_next_after: Option<NameId>,
|
||||
}
|
||||
|
||||
pub fn load_all_paged(
|
||||
store: &dyn Storage,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<NameId>,
|
||||
) -> Result<PagedLoad> {
|
||||
let limit = limit
|
||||
.unwrap_or(NAME_DEFAULT_RETRIEVAL_LIMIT)
|
||||
.min(NAME_MAX_RETRIEVAL_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
let names = names()
|
||||
.range(store, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = names.last().map(|name| name.0);
|
||||
|
||||
Ok(PagedLoad {
|
||||
names,
|
||||
limit,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter::zip;
|
||||
|
||||
use cosmwasm_std::{
|
||||
testing::{MockApi, MockQuerier},
|
||||
MemoryStorage, OwnedDeps,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
error::NameServiceError,
|
||||
test_helpers::{
|
||||
fixture::{name_fixture, name_fixture_full},
|
||||
helpers::instantiate_test_contract,
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
type TestDeps = OwnedDeps<MemoryStorage, MockApi, MockQuerier>;
|
||||
#[rstest::fixture]
|
||||
fn deps() -> TestDeps {
|
||||
instantiate_test_contract()
|
||||
}
|
||||
|
||||
#[rstest::fixture]
|
||||
fn uniq_names() -> Vec<RegisteredName> {
|
||||
vec![
|
||||
name_fixture_full("one", "address_one", "owner_one"),
|
||||
name_fixture_full("two", "address_two", "owner_two"),
|
||||
name_fixture_full("three", "address_three", "owner_three"),
|
||||
]
|
||||
}
|
||||
|
||||
#[rstest::fixture]
|
||||
fn overlapping_addresses() -> Vec<RegisteredName> {
|
||||
vec![
|
||||
name_fixture_full("one", "address_one", "owner_one"),
|
||||
name_fixture_full("two", "address_two", "owner_two"),
|
||||
name_fixture_full("three", "address_two", "owner_three"),
|
||||
]
|
||||
}
|
||||
|
||||
#[rstest::fixture]
|
||||
fn overlapping_owners() -> Vec<RegisteredName> {
|
||||
vec![
|
||||
name_fixture_full("one", "address_one", "owner_one"),
|
||||
name_fixture_full("two", "address_two", "owner_two"),
|
||||
name_fixture_full("three", "address_three", "owner_two"),
|
||||
]
|
||||
}
|
||||
|
||||
fn assert_not_registered(store: &dyn Storage, names: Vec<RegisteredName>, ids: Vec<NameId>) {
|
||||
let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect();
|
||||
let loaded = load_all_paged(store, None, None).unwrap();
|
||||
for (id, name) in &names {
|
||||
assert!(!has_name_id(store, *id));
|
||||
assert!(!has_name(store, &name.name));
|
||||
assert!(!loaded.names.iter().any(|(i, _)| i == id));
|
||||
assert!(!loaded.names.iter().any(|(_, n)| n == name));
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_registered(store: &dyn Storage, names: Vec<RegisteredName>, ids: Vec<NameId>) {
|
||||
assert!(names.len() == ids.len());
|
||||
let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect();
|
||||
let loaded = load_all_paged(store, None, None).unwrap();
|
||||
for (id, name) in &names {
|
||||
assert!(has_name_id(store, *id));
|
||||
assert!(has_name(store, &name.name));
|
||||
assert!(loaded.names.iter().filter(|(i, _)| i == id).count() == 1);
|
||||
assert!(loaded.names.iter().filter(|(_, n)| n == name).count() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_only_these_registered(
|
||||
store: &dyn Storage,
|
||||
names: Vec<RegisteredName>,
|
||||
ids: Vec<NameId>,
|
||||
) {
|
||||
let last_id = *ids.last().unwrap();
|
||||
let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect();
|
||||
for (id, name) in &names {
|
||||
assert!(has_name_id(store, *id));
|
||||
assert!(has_name(store, &name.name));
|
||||
}
|
||||
assert_eq!(
|
||||
load_all_paged(store, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names,
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(last_id),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn single_basic_save_works(mut deps: TestDeps) {
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_same_name_twice_fails(mut deps: TestDeps) {
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(matches!(
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap_err(),
|
||||
NameServiceError::Std(StdError::GenericErr { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn has_name_works(mut deps: TestDeps) {
|
||||
assert!(!has_name(&deps.storage, &name_fixture().name));
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(has_name(&deps.storage, &name_fixture().name));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn has_name_id_works(mut deps: TestDeps) {
|
||||
assert!(!has_name_id(&deps.storage, 1));
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(has_name_id(&deps.storage, 1));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn has_name_id_with_incorrect_id_fails(mut deps: TestDeps) {
|
||||
assert!(!has_name_id(&deps.storage, 2));
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(!has_name_id(&deps.storage, 2));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_name_entry_works(mut deps: TestDeps) {
|
||||
assert_eq!(
|
||||
load_name_entry(deps.as_ref().storage, &name_fixture().name).unwrap_err(),
|
||||
NameServiceError::NameNotFound {
|
||||
name: name_fixture().name
|
||||
}
|
||||
);
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert_eq!(
|
||||
load_name_entry(deps.as_ref().storage, &name_fixture().name).unwrap(),
|
||||
(1, name_fixture())
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_id_works(mut deps: TestDeps) {
|
||||
assert_eq!(
|
||||
load_id(deps.as_ref().storage, 1).unwrap_err(),
|
||||
NameServiceError::NotFound { name_id: 1 }
|
||||
);
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert_eq!(load_id(deps.as_ref().storage, 1).unwrap(), name_fixture(),);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_name_works(mut deps: TestDeps) {
|
||||
assert_eq!(
|
||||
load_name(deps.as_ref().storage, &name_fixture().name).unwrap_err(),
|
||||
NameServiceError::NameNotFound {
|
||||
name: name_fixture().name
|
||||
}
|
||||
);
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert_eq!(
|
||||
load_name(deps.as_ref().storage, &name_fixture().name).unwrap(),
|
||||
name_fixture(),
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_address_works(mut deps: TestDeps) {
|
||||
assert_eq!(
|
||||
load_address(deps.as_ref().storage, &name_fixture().address).unwrap(),
|
||||
vec![],
|
||||
);
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert_eq!(
|
||||
load_address(deps.as_ref().storage, &name_fixture().address).unwrap(),
|
||||
vec![(1, name_fixture())],
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_owner_works(mut deps: TestDeps) {
|
||||
assert_eq!(
|
||||
load_owner(deps.as_ref().storage, name_fixture().owner).unwrap(),
|
||||
vec![],
|
||||
);
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert_eq!(
|
||||
load_owner(deps.as_ref().storage, name_fixture().owner).unwrap(),
|
||||
vec![(1, name_fixture())],
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_all_paged_works(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
assert_eq!(
|
||||
load_all_paged(&deps.storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: None
|
||||
}
|
||||
);
|
||||
save(deps.as_mut().storage, &uniq_names[0]).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(&deps.storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![(1, uniq_names[0].clone())],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(1),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn remove_id_works(mut deps: TestDeps) {
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(has_name_id(&deps.storage, 1));
|
||||
assert!(has_name(&deps.storage, &name_fixture().name));
|
||||
remove_id(deps.as_mut().storage, 1).unwrap();
|
||||
assert!(!has_name_id(&deps.storage, 1));
|
||||
assert!(!has_name(&deps.storage, &name_fixture().name));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn remove_name_works(mut deps: TestDeps) {
|
||||
save(deps.as_mut().storage, &name_fixture()).unwrap();
|
||||
assert!(has_name_id(&deps.storage, 1));
|
||||
assert!(has_name(&deps.storage, &name_fixture().name));
|
||||
remove_name(deps.as_mut().storage, name_fixture().name).unwrap();
|
||||
assert!(!has_name_id(&deps.storage, 1));
|
||||
assert!(!has_name(&deps.storage, &name_fixture().name));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_set_of_unique_names_works(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
let num = uniq_names.len() as NameId;
|
||||
let ids = (1..=num).collect::<Vec<NameId>>();
|
||||
assert_not_registered(&deps.storage, uniq_names.clone(), ids.clone());
|
||||
let saved_ids = save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
assert_eq!(saved_ids, ids);
|
||||
assert_registered(&deps.storage, uniq_names.clone(), ids.clone());
|
||||
assert_only_these_registered(&deps.storage, uniq_names, ids);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_set_of_unique_names_generates_ids(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
let num = uniq_names.len() as NameId;
|
||||
let ids = save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
assert_eq!(ids, (1..=num).collect::<Vec<NameId>>());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_name_entry_for_unique_set_works(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
for (id, name) in uniq_names.iter().enumerate() {
|
||||
assert_eq!(
|
||||
load_name_entry(deps.as_ref().storage, &name.name).unwrap(),
|
||||
(id as NameId + 1, name.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_name_for_unique_set_works(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
for name in uniq_names {
|
||||
assert_eq!(
|
||||
load_name(deps.as_ref().storage, &name.name).unwrap(),
|
||||
name.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_id_from_set_of_unique_set_works(
|
||||
mut deps: TestDeps,
|
||||
uniq_names: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
remove_id(deps.as_mut().storage, ids[1]).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(
|
||||
3,
|
||||
name_fixture_full("three", "address_three", "owner_three")
|
||||
),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_from_set_of_unique_set_works(
|
||||
mut deps: TestDeps,
|
||||
uniq_names: Vec<RegisteredName>,
|
||||
) {
|
||||
save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
remove_name(deps.as_mut().storage, uniq_names[1].name.clone()).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(
|
||||
3,
|
||||
name_fixture_full("three", "address_three", "owner_three")
|
||||
),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_set_of_overlapping_addressed_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_addresses: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &overlapping_addresses).unwrap();
|
||||
assert_registered(&deps.storage, overlapping_addresses.clone(), ids.clone());
|
||||
assert_only_these_registered(&deps.storage, overlapping_addresses, ids);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_address_with_overlapping_addresses_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_addresses: Vec<RegisteredName>,
|
||||
) {
|
||||
save_all(deps.as_mut().storage, &overlapping_addresses).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(2, name_fixture_full("two", "address_two", "owner_two")),
|
||||
(3, name_fixture_full("three", "address_two", "owner_three")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
load_address(deps.as_ref().storage, &Address::new("address_two")).unwrap(),
|
||||
vec![
|
||||
(2, name_fixture_full("two", "address_two", "owner_two")),
|
||||
(3, name_fixture_full("three", "address_two", "owner_three")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_id_from_set_of_overlapping_addresses_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_addresses: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &overlapping_addresses).unwrap();
|
||||
remove_id(deps.as_mut().storage, ids[1]).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(3, name_fixture_full("three", "address_two", "owner_three")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_from_set_of_overlapping_addresses_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_addresses: Vec<RegisteredName>,
|
||||
) {
|
||||
save_all(deps.as_mut().storage, &overlapping_addresses).unwrap();
|
||||
remove_name(deps.as_mut().storage, overlapping_addresses[1].name.clone()).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(3, name_fixture_full("three", "address_two", "owner_three")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_set_of_overlapping_owners_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_owners: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap();
|
||||
assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone());
|
||||
assert_only_these_registered(&deps.storage, overlapping_owners, ids);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_owner_with_overlapping_owners_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_owners: Vec<RegisteredName>,
|
||||
) {
|
||||
save_all(deps.as_mut().storage, &overlapping_owners).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(2, name_fixture_full("two", "address_two", "owner_two")),
|
||||
(3, name_fixture_full("three", "address_three", "owner_two")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
load_owner(deps.as_ref().storage, Addr::unchecked("owner_two")).unwrap(),
|
||||
vec![
|
||||
(2, name_fixture_full("two", "address_two", "owner_two")),
|
||||
(3, name_fixture_full("three", "address_three", "owner_two")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_id_from_set_of_overlapping_owners_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_owners: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap();
|
||||
assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone());
|
||||
assert_only_these_registered(&deps.storage, overlapping_owners, ids);
|
||||
remove_id(deps.as_mut().storage, 2).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(3, name_fixture_full("three", "address_three", "owner_two")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn save_and_remove_name_from_set_of_overlapping_owners_works(
|
||||
mut deps: TestDeps,
|
||||
overlapping_owners: Vec<RegisteredName>,
|
||||
) {
|
||||
let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap();
|
||||
assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone());
|
||||
assert_only_these_registered(&deps.storage, overlapping_owners.clone(), ids);
|
||||
remove_name(deps.as_mut().storage, overlapping_owners[1].name.clone()).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(deps.as_ref().storage, None, None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(3, name_fixture_full("three", "address_three", "owner_two")),
|
||||
],
|
||||
limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn load_all_paged_with_limit_works(mut deps: TestDeps, uniq_names: Vec<RegisteredName>) {
|
||||
save_all(deps.as_mut().storage, &uniq_names).unwrap();
|
||||
assert_eq!(
|
||||
load_all_paged(&deps.storage, Some(2), None).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![
|
||||
(1, name_fixture_full("one", "address_one", "owner_one")),
|
||||
(2, name_fixture_full("two", "address_two", "owner_two")),
|
||||
],
|
||||
limit: 2,
|
||||
start_next_after: Some(2),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
load_all_paged(&deps.storage, Some(1), Some(2)).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![(
|
||||
3,
|
||||
name_fixture_full("three", "address_three", "owner_three")
|
||||
),],
|
||||
limit: 1,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
load_all_paged(&deps.storage, Some(2), Some(2)).unwrap(),
|
||||
PagedLoad {
|
||||
names: vec![(
|
||||
3,
|
||||
name_fixture_full("three", "address_three", "owner_three")
|
||||
),],
|
||||
limit: 2,
|
||||
start_next_after: Some(3),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn max_page_limit_is_applied() {
|
||||
// WIP(JON)
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use cosmwasm_std::{from_binary, testing::mock_env, Addr, Coin, Deps};
|
||||
use nym_name_service_common::{
|
||||
msg::QueryMsg,
|
||||
response::{ConfigResponse, PagedNamesListResponse},
|
||||
NameEntry, NameId,
|
||||
};
|
||||
|
||||
use crate::{constants::NAME_DEFAULT_RETRIEVAL_LIMIT, error::NameServiceError};
|
||||
|
||||
pub fn assert_config(deps: Deps, admin: &Addr, deposit_required: Coin) {
|
||||
crate::state::assert_admin(deps, admin).unwrap();
|
||||
let res = crate::contract::query(deps, mock_env(), QueryMsg::Config {}).unwrap();
|
||||
let config: ConfigResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(config, ConfigResponse { deposit_required });
|
||||
}
|
||||
|
||||
pub fn assert_names(deps: Deps, expected_names: &[NameEntry]) {
|
||||
let res = crate::contract::query(deps, mock_env(), QueryMsg::all()).unwrap();
|
||||
let names: PagedNamesListResponse = from_binary(&res).unwrap();
|
||||
let start_next_after = expected_names.iter().last().map(|s| s.name_id);
|
||||
assert_eq!(
|
||||
names,
|
||||
PagedNamesListResponse {
|
||||
names: expected_names.to_vec(),
|
||||
per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize,
|
||||
start_next_after,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
pub fn assert_name(deps: Deps, expected_name: &NameEntry) {
|
||||
let res = crate::contract::query(
|
||||
deps,
|
||||
mock_env(),
|
||||
QueryMsg::NameId {
|
||||
name_id: expected_name.name_id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let names: NameEntry = from_binary(&res).unwrap();
|
||||
assert_eq!(&names, expected_name);
|
||||
}
|
||||
|
||||
pub fn assert_empty(deps: Deps) {
|
||||
let res = crate::contract::query(deps, mock_env(), QueryMsg::all()).unwrap();
|
||||
let names: PagedNamesListResponse = from_binary(&res).unwrap();
|
||||
assert!(names.names.is_empty());
|
||||
}
|
||||
|
||||
pub fn assert_not_found(deps: Deps, expected_id: NameId) {
|
||||
let res = crate::contract::query(
|
||||
deps,
|
||||
mock_env(),
|
||||
QueryMsg::NameId {
|
||||
name_id: expected_id,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
res,
|
||||
NameServiceError::NotFound {
|
||||
name_id: _expected_id
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_name_service_common::{Address, NameEntry, NameId, NymName, RegisteredName};
|
||||
|
||||
use super::helpers::nyms;
|
||||
|
||||
pub fn name_fixture_full(name: &str, nym_address: &str, owner: &str) -> RegisteredName {
|
||||
RegisteredName {
|
||||
name: NymName::new(name).unwrap(),
|
||||
address: Address::new(nym_address),
|
||||
owner: Addr::unchecked(owner),
|
||||
block_height: 12345,
|
||||
deposit: nyms(100),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name_fixture() -> RegisteredName {
|
||||
name_fixture_full("my-service", "client_id.client_key@gateway_id", "steve")
|
||||
}
|
||||
|
||||
pub fn name_fixture_name(name: &str) -> RegisteredName {
|
||||
name_fixture_full(name, "client_id.client_key@gateway_id", "steve")
|
||||
}
|
||||
|
||||
pub fn name_entry(name_id: NameId, name: NymName, address: Address, owner: Addr) -> NameEntry {
|
||||
NameEntry {
|
||||
name_id,
|
||||
name: name_fixture_full(name.as_str(), address.as_str(), owner.as_str()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use cosmwasm_std::{
|
||||
coin, coins,
|
||||
testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier},
|
||||
Coin, DepsMut, Event, MemoryStorage, OwnedDeps, Response,
|
||||
};
|
||||
use cw_multi_test::AppResponse;
|
||||
use nym_name_service_common::{
|
||||
events::{NameEventType, NAME_ID},
|
||||
msg::{ExecuteMsg, InstantiateMsg},
|
||||
NameId, NymName, RegisteredName,
|
||||
};
|
||||
|
||||
pub fn nyms(amount: u64) -> Coin {
|
||||
Coin::new(amount.into(), "unym")
|
||||
}
|
||||
|
||||
pub fn get_event_types(response: &Response, event_type: &str) -> Vec<Event> {
|
||||
response
|
||||
.events
|
||||
.iter()
|
||||
.filter(|ev| ev.ty == event_type)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_attribute(response: &Response, event_type: &str, key: &str) -> String {
|
||||
get_event_types(response, event_type)
|
||||
.first()
|
||||
.unwrap()
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == key)
|
||||
.unwrap()
|
||||
.value
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn get_app_event_types(response: &AppResponse, event_type: &str) -> Vec<Event> {
|
||||
response
|
||||
.events
|
||||
.iter()
|
||||
.filter(|ev| ev.ty == event_type)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_app_attribute(response: &AppResponse, event_type: &str, key: &str) -> String {
|
||||
get_app_event_types(response, event_type)
|
||||
.first()
|
||||
.unwrap()
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == key)
|
||||
.unwrap()
|
||||
.value
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_app_attributes(response: &AppResponse, event_type: &str, key: &str) -> Vec<String> {
|
||||
get_app_event_types(response, event_type)
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
ev.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == key)
|
||||
.unwrap()
|
||||
.value
|
||||
.clone()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn instantiate_test_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier> {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg {
|
||||
deposit_required: coin(100, "unym"),
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info("creator", &[]);
|
||||
let res = crate::instantiate(deps.as_mut(), env, info, msg).unwrap();
|
||||
assert_eq!(res.messages.len(), 0);
|
||||
deps
|
||||
}
|
||||
|
||||
pub fn register_name(deps: DepsMut<'_>, name: &RegisteredName) -> NameId {
|
||||
let msg: ExecuteMsg = name.clone().into();
|
||||
let info = mock_info(name.owner.as_str(), &coins(100, "unym"));
|
||||
let res = crate::execute(deps, mock_env(), info, msg).unwrap();
|
||||
let name_id: NameId = get_attribute(&res, &NameEventType::Register.to_string(), NAME_ID)
|
||||
.parse()
|
||||
.unwrap();
|
||||
name_id
|
||||
}
|
||||
|
||||
pub fn delete_name_id(deps: DepsMut<'_>, name_id: NameId, owner: &str) {
|
||||
let msg = ExecuteMsg::DeleteId { name_id };
|
||||
let info = mock_info(owner, &[]);
|
||||
crate::execute(deps, mock_env(), info, msg).unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn delete_name(deps: DepsMut<'_>, name: NymName, owner: &str) {
|
||||
let msg = ExecuteMsg::DeleteName { name };
|
||||
let info = mock_info(owner, &[]);
|
||||
crate::execute(deps, mock_env(), info, msg).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod assert;
|
||||
pub mod fixture;
|
||||
pub mod helpers;
|
||||
pub mod test_setup;
|
||||
@@ -0,0 +1,153 @@
|
||||
use cosmwasm_std::{coins, Addr, Coin, Uint128};
|
||||
use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor};
|
||||
use nym_name_service_common::{
|
||||
msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
|
||||
response::{ConfigResponse, PagedNamesListResponse},
|
||||
Address, NameEntry, NameId, NymName,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::test_helpers::helpers::get_app_attribute;
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
const ADDRESSES: &[&str] = &[
|
||||
"user", "admin", "owner", "owner1", "owner2", "owner3", "owner4",
|
||||
];
|
||||
const WEALTHY_ADDRESSES: &[&str] = &["wealthy_owner_1", "wealthy_owner_2"];
|
||||
|
||||
/// Helper for being able to systematic integration tests
|
||||
pub struct TestSetup {
|
||||
app: App,
|
||||
addr: Addr,
|
||||
}
|
||||
|
||||
impl Default for TestSetup {
|
||||
fn default() -> Self {
|
||||
TestSetup::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSetup {
|
||||
pub fn new() -> Self {
|
||||
let mut app = AppBuilder::new().build(|router, _, storage| {
|
||||
let mut init_balance = |account: &str, amount: u128| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(storage, &Addr::unchecked(account), coins(amount, DENOM))
|
||||
.unwrap();
|
||||
};
|
||||
ADDRESSES.iter().for_each(|addr| init_balance(addr, 250));
|
||||
WEALTHY_ADDRESSES
|
||||
.iter()
|
||||
.for_each(|addr| init_balance(addr, 1000));
|
||||
});
|
||||
let code = ContractWrapper::new(crate::execute, crate::instantiate, crate::query);
|
||||
let code_id = app.store_code(Box::new(code));
|
||||
let addr = Self::instantiate(&mut app, code_id);
|
||||
TestSetup { app, addr }
|
||||
}
|
||||
|
||||
fn instantiate(app: &mut App, code_id: u64) -> Addr {
|
||||
app.instantiate_contract(
|
||||
code_id,
|
||||
Addr::unchecked("admin"),
|
||||
&InstantiateMsg {
|
||||
deposit_required: Coin::new(100, DENOM),
|
||||
},
|
||||
&[],
|
||||
"contract_label",
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn contract_balance(&self) -> Coin {
|
||||
self.app.wrap().query_balance(&self.addr, DENOM).unwrap()
|
||||
}
|
||||
|
||||
pub fn query<T: DeserializeOwned>(&self, query_msg: &QueryMsg) -> T {
|
||||
self.app
|
||||
.wrap()
|
||||
.query_wasm_smart(&self.addr, query_msg)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn query_config(&self) -> ConfigResponse {
|
||||
self.query(&QueryMsg::Config {})
|
||||
}
|
||||
|
||||
pub fn query_id(&self, name_id: NameId) -> NameEntry {
|
||||
self.query(&QueryMsg::NameId { name_id })
|
||||
}
|
||||
|
||||
pub fn query_all(&self) -> PagedNamesListResponse {
|
||||
self.query(&QueryMsg::all())
|
||||
}
|
||||
|
||||
pub fn query_all_with_limit(
|
||||
&self,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<u32>,
|
||||
) -> PagedNamesListResponse {
|
||||
self.query(&QueryMsg::All { limit, start_after })
|
||||
}
|
||||
|
||||
pub fn try_register(
|
||||
&mut self,
|
||||
name: NymName,
|
||||
address: Address,
|
||||
owner: Addr,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.app.execute_contract(
|
||||
owner,
|
||||
self.addr.clone(),
|
||||
&ExecuteMsg::Register { name, address },
|
||||
&[Coin {
|
||||
denom: DENOM.to_string(),
|
||||
amount: Uint128::new(100),
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn register(&mut self, name: NymName, address: Address, owner: Addr) -> AppResponse {
|
||||
let resp = self.try_register(name, address, owner).unwrap();
|
||||
assert_eq!(
|
||||
get_app_attribute(&resp, "wasm-register", "action"),
|
||||
"register"
|
||||
);
|
||||
resp
|
||||
}
|
||||
|
||||
pub fn try_delete(&mut self, name_id: NameId, owner: Addr) -> anyhow::Result<AppResponse> {
|
||||
self.app.execute_contract(
|
||||
owner,
|
||||
self.addr.clone(),
|
||||
&ExecuteMsg::DeleteId { name_id },
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, name_id: NameId, owner: Addr) -> AppResponse {
|
||||
let delete_resp = self.try_delete(name_id, owner).unwrap();
|
||||
assert_eq!(
|
||||
get_app_attribute(&delete_resp, "wasm-delete_id", "action"),
|
||||
"delete_id"
|
||||
);
|
||||
delete_resp
|
||||
}
|
||||
|
||||
pub fn delete_name(&mut self, name: NymName, owner: Addr) -> AppResponse {
|
||||
self.app
|
||||
.execute_contract(
|
||||
owner,
|
||||
self.addr.clone(),
|
||||
&ExecuteMsg::DeleteName { name },
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn balance(&self, address: impl Into<String>) -> Coin {
|
||||
self.app.wrap().query_balance(address, DENOM).unwrap()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user