feat: rosetta data api and construction validation with rosetta-cli

* test: added test for rosetta cli check:construction and check:data

* ci: added github workflow to test  roseta-cli:data

* ci: add github action for rosetta-cli: construction

* refactor: refactor cli tests

* fix: use localhoust for github actions in rosetta config file

* fix: github actions

* fix: rebase issue

* refactor: update rosetta-cli env files path

* refactor: remove hard coded cli container and use rosetta-cli ouput

* perf: remove unnecessary wait of 20 seconds
This commit is contained in:
Asim Mehmood
2021-11-16 21:18:04 +05:00
committed by GitHub
parent 285632a983
commit f764054cb8
26 changed files with 975 additions and 404 deletions

View File

@@ -266,6 +266,126 @@ jobs:
uses: codecov/codecov-action@v1
if: always()
test-rosetta-cli-data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
check-latest: true
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install deps
run: npm install --audit=false
- name: Setup env vars
run: echo "STACKS_CORE_EVENT_HOST=http://0.0.0.0">> $GITHUB_ENV
- name: Setup cli enviroment variable
run: |
echo STACKS_BLOCKCHAIN_API_HOST=0.0.0.0>> .env
echo STACKS_CORE_PROXY_HOST=0.0.0.0 >> .env
echo STACKS_CORE_RPC_HOST=0.0.0.0 >> .env
echo STACKS_CORE_EVENT_HOST=0.0.0.0 >> .env
echo BTC_RPC_HOST=http://0.0.0.0 >> .env
- name: Setup integration environment
run: |
sudo ufw disable
npm run devenv:deploy -- -d
npm run devenv:logs -- --no-color &> docker-compose-logs.txt &
- name: Run tests
run: sudo npm run test:rosetta-cli:data
- name: Print integration environment logs
run: cat docker-compose-logs.txt
if: failure()
- name: Teardown integration environment
run: npm run devenv:stop
if: always()
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
if: always()
test-rosetta-cli-construction:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
check-latest: true
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install deps
run: npm install --audit=false
- name: Setup env vars
run: echo "STACKS_CORE_EVENT_HOST=http://0.0.0.0">> $GITHUB_ENV
- name: Setup cli enviroment variable
run: |
echo STACKS_BLOCKCHAIN_API_HOST=0.0.0.0>> .env
echo STACKS_CORE_PROXY_HOST=0.0.0.0 >> .env
echo STACKS_CORE_RPC_HOST=0.0.0.0 >> .env
echo STACKS_CORE_EVENT_HOST=0.0.0.0 >> .env
echo BTC_RPC_HOST=http://0.0.0.0 >> .env
- name: Setup integration environment
run: |
sudo ufw disable
npm run devenv:deploy -- -d
npm run devenv:logs -- --no-color &> docker-compose-logs.txt &
- name: Run tests
run: sudo npm run test:rosetta-cli:construction
- name: Print integration environment logs
run: cat docker-compose-logs.txt
if: failure()
- name: Teardown integration environment
run: npm run devenv:stop
if: always()
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
if: always()
test-tokens:
runs-on: ubuntu-latest
steps:

4
.gitignore vendored
View File

@@ -126,3 +126,7 @@ yarn.lock
# Git info output
.git-info
#rosetta-cli results
rosetta-output-construction/rosetta-cli-output-const.json
rosetta-output/rosetta-cli-output.json

View File

@@ -4,4 +4,7 @@ services:
build:
context: ./rosetta-cli-config
dockerfile: docker/Dockerfile
command: /bin/rosetta-cli --configuration-file /app/rosetta-config-docker.json check:construction
command: ${CMD}
volumes:
- ${OUTPUT}

View File

@@ -0,0 +1,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: 'src',
testMatch: ['<rootDir>/tests-rosetta-cli-construction/**/*.ts'],
testPathIgnorePatterns: ['<rootDir>/tests-rosetta-cli-construction/setup.ts', '<rootDir>/tests-rosetta-cli-construction/teardown.ts'],
collectCoverageFrom: ['<rootDir>/**/*.ts'],
coveragePathIgnorePatterns: ['<rootDir>/tests'],
coverageDirectory: '../coverage',
globalSetup: '<rootDir>/tests-rosetta-cli-construction/setup.ts',
globalTeardown: '<rootDir>/tests-rosetta-cli-construction/teardown.ts',
testTimeout: 180000,
transformIgnorePatterns: [
"node_modules/(?!(@stacks/stacks-transactions)/)"
]
}

View File

@@ -0,0 +1,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: 'src',
testMatch: ['<rootDir>/tests-rosetta-cli-data/**/*.ts'],
testPathIgnorePatterns: ['<rootDir>/tests-rosetta-cli-data/setup.ts', '<rootDir>/tests-rosetta-cli-data/teardown.ts'],
collectCoverageFrom: ['<rootDir>/**/*.ts'],
coveragePathIgnorePatterns: ['<rootDir>/tests'],
coverageDirectory: '../coverage',
globalSetup: '<rootDir>/tests-rosetta-cli-data/setup.ts',
globalTeardown: '<rootDir>/tests-rosetta-cli-data/teardown.ts',
testTimeout: 240000,
transformIgnorePatterns: [
"node_modules/(?!(@stacks/stacks-transactions)/)"
]
};

View File

@@ -1,16 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: 'src',
testMatch: ['<rootDir>/tests-rosetta-cli/validate-rosetta-construction.ts'],
testPathIgnorePatterns: [
'<rootDir>/tests-rosetta-cli/setup.ts',
'<rootDir>/tests-rosetta-cli/teardown.ts',
],
collectCoverageFrom: ['<rootDir>/**/*.ts'],
coveragePathIgnorePatterns: ['<rootDir>/tests'],
coverageDirectory: '../coverage',
globalSetup: '<rootDir>/tests-rosetta-cli/setup.ts',
globalTeardown: '<rootDir>/tests-rosetta-cli/teardown.ts',
testTimeout: 180000,
};

122
package-lock.json generated
View File

@@ -3832,6 +3832,16 @@
"wif": "^2.0.1"
}
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -4317,6 +4327,11 @@
"readdirp": "~3.5.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -5080,6 +5095,15 @@
"yaml": "^1.7.2"
}
},
"cpu-features": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz",
"integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==",
"optional": true,
"requires": {
"nan": "^2.14.1"
}
},
"create-ecdh": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
@@ -5513,6 +5537,41 @@
"yaml": "^1.10.2"
}
},
"docker-modem": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz",
"integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==",
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.4.0"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"dockerode": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz",
"integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==",
"requires": {
"docker-modem": "^3.0.0",
"tar-fs": "~2.0.1"
}
},
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -5714,7 +5773,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
@@ -7277,6 +7335,11 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -12896,6 +12959,11 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"mobx": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.0.tgz",
@@ -13685,7 +13753,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@@ -14525,7 +14592,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -16135,6 +16201,11 @@
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
"dev": true
},
"split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -16157,6 +16228,25 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"ssh2": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.4.0.tgz",
"integrity": "sha512-XvXwcXKvS452DyQvCa6Ct+chpucwc/UyxgliYz+rWXJ3jDHdtBb9xgmxJdMmnIn5bpgGAEV3KaEsH98ZGPHqwg==",
"requires": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2",
"cpu-features": "0.0.2",
"nan": "^2.15.0"
},
"dependencies": {
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"optional": true
}
}
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@@ -16623,6 +16713,29 @@
}
}
},
"tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
},
"tdigest": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
@@ -17977,8 +18090,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
"version": "3.0.3",

View File

@@ -9,6 +9,8 @@
"dev:follower": "npm run devenv:build && concurrently npm:dev npm:devenv:follower",
"test": "cross-env NODE_ENV=development jest --config ./jest.config.js --coverage --runInBand",
"test:rosetta": "cross-env NODE_ENV=development jest --config ./jest.config.rosetta.js --coverage --runInBand",
"test:rosetta-cli:data": "cross-env NODE_ENV=development STACKS_CHAIN_ID=0x80000000 jest --config ./jest.config.rosetta-cli-data.js --runInBand",
"test:rosetta-cli:construction": "cross-env NODE_ENV=development STACKS_CHAIN_ID=0x80000000 jest --config ./jest.config.rosetta-cli-construction.js --runInBand",
"test:bns": "cross-env NODE_ENV=development jest --config ./jest.config.bns.js --coverage --runInBand",
"test:microblocks": "cross-env NODE_ENV=development jest --config ./jest.config.microblocks.js --coverage --runInBand",
"test:tokens": "cross-env NODE_ENV=development jest --config ./jest.config.tokens.js --coverage --runInBand",
@@ -28,7 +30,7 @@
"lint:prettier": "prettier --check src/**/*.{ts,json}",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe --fix && prettier --write --check src/**/*.{ts,json}",
"migrate": "node-pg-migrate -m src/migrations",
"devenv:build": "docker-compose -f docker-compose.dev.postgres.yml -f docker-compose.dev.stacks-blockchain.yml -f docker-compose.dev.bitcoind.yml build --no-cache",
"devenv:build": "docker-compose -f docker-compose.dev.postgres.yml -f docker-compose.dev.stacks-blockchain.yml -f docker-compose.dev.bitcoind.yml -f docker-compose.dev.rosetta-cli.yml build --no-cache",
"devenv:deploy": "docker-compose -f docker-compose.dev.postgres.yml -f docker-compose.dev.stacks-blockchain.yml -f docker-compose.dev.bitcoind.yml up",
"devenv:deploy:pg": "docker-compose -f docker-compose.dev.postgres.yml up",
"devenv:follower": "docker-compose -f docker-compose.dev.postgres.yml -f docker-compose.dev.stacks-blockchain-follower.yml up",
@@ -121,6 +123,7 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"cross-env": "^7.0.2",
"dockerode": "^3.3.1",
"dotenv": "^8.2.0",
"dotenv-flow": "^3.2.0",
"escape-goat": "^3.0.0",

View File

@@ -0,0 +1,28 @@
FROM everpeace/curl-jq as build
RUN mkdir -p /bin
ENV ROSETTA_CLI_VERSION "0.5.10"
RUN curl -L --output rosetta-cli-${ROSETTA_CLI_VERSION}-linux-amd64.tar.gz \
https://github.com/coinbase/rosetta-cli/releases/download/v${ROSETTA_CLI_VERSION}/rosetta-cli-${ROSETTA_CLI_VERSION}-linux-amd64.tar.gz \
&& tar xzf rosetta-cli-${ROSETTA_CLI_VERSION}-linux-amd64.tar.gz \
&& mv rosetta-cli-${ROSETTA_CLI_VERSION}-linux-amd64 /bin/rosetta-cli \
&& chmod +x /bin/rosetta-cli
FROM debian:stretch
RUN mkdir -p /bin /app
COPY --from=build /bin/rosetta-cli /bin/
COPY docker/docker-entrypoint.sh /bin
RUN chmod +x /bin/docker-entrypoint.sh
COPY . /app
WORKDIR /app
ENTRYPOINT ["/bin/docker-entrypoint.sh"]
# CMD ["/bin/rosetta-cli", "--configuration-file /app/rosetta-config-docker.json", "view:networks"]

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Use this script to enable host.docker.internal on Docker for linux.
# See https://github.com/bufferings/docker-access-host
HOST_DOMAIN="host.docker.internal"
ping -q -c1 $HOST_DOMAIN >/dev/null 2>&1
if [ $? -ne 0 ]; then
HOST_IP=$(ip route | awk 'NR==1 {print $3}')
echo -e "$HOST_IP\t$HOST_DOMAIN" >>/etc/hosts
fi
cat /etc/hosts
echo "patched host.docker.internal"
exec "$@"

View File

@@ -34,5 +34,32 @@
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST18MDW2PDTBSCR1ACXYRJP2JX70FWNM6YY2VX4SS"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST000000000000000000002AMW42H"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
}
]

View File

@@ -0,0 +1,191 @@
[
{
"account_identifier": {
"address": "ST1DTAEAKM02GKCT4NGKTVER8MTJJHYQ9NT27E677"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3KTZ45AQES4PNNHB2YGGJ4JXQQMRACRNZPQ19SP"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST17ZNMSQMDARSCZ85Z7BVJX6T20ZWDF3VX0ZP33K"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3DR3THSKSWRH10SEDFFP3D90KZYG57J5N8M9KR9"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3WHD5WEKHENA38T9BWZK9M0SDXA46T9DQT0ZDTY"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST2WZKZVZRJJMT2RJPWWM9FVC20S9R30CNVMJEX0H"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "STFHWET10QDAQ8B88WNAVEV15XVNWNNCTMQGD5KN"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3YFNTA33A14NYTE0P85790E2X7Y890SD24K8WAK"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST367HN911AFQ666829M2C6XP0HPMSTT517B5FMNB"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "STN5W5SAFHVWS9P6BDMVJQZWQGCAHQBPXQYKKCCW"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST5WDT0C4995DWA3H7YDKYM0NK4QNADR4T93HYD1"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST1JBBKAPX6XDGR9TEGQTNVV2H4NJX456FKRJ5QWA"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3T6ERPJ2TXPQ668747Z52P4EJBYY966SVFM7W8X"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST2Y60MJ5CPGEN1CP405H7X8CPCEYV2FJ0PHZ742P"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "STAXJZ26VDBNA5TVJRJ63SV77C1AWW786X84V634"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3Q362NAVV3W6T1GV0XH78JJJ0BKZREHR6C168TV"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST16EDNWEW80AX2737KA58SJZQ8QNW7DXKTDBBVCV"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST3VHZ1ETEB0D7V27TCB5BB9YVQVWPQJHJG5Y6ARH"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST2NC3GSVB511TPCBDKB4PRA6VJHYNSGH93756H1M"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "ST11QXGG0SRPC5QYESX3WA55BKQWWMPV7CSWKG6SN"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"account_identifier": {
"address": "STJ5PW80NKC6NJ0J87YWXYNGE474RN38K2B4YW1R"
},
"currency": {
"symbol": "STX",
"decimals": 6
}
}
]

View File

@@ -6,9 +6,7 @@
"online_url": "http://host.docker.internal:3999/rosetta/v1",
"data_directory": "",
"http_timeout": 10,
"sync_concurrency": 8,
"transaction_concurrency": 16,
"tip_delay": 5,
"tip_delay": 500,
"data": {
"active_reconciliation_concurrency": 16,
"inactive_reconciliation_concurrency": 4,
@@ -18,80 +16,70 @@
"log_balance_changes": true,
"log_reconciliations": false,
"ignore_reconciliation_error": false,
"exempt_accounts": "/app/exempt_accounts.json",
"bootstrap_balances": "/app/bootstrap_balances.json",
"interesting_accounts": "/app/interesting_accounts.json",
"historical_balance_disabled": true,
"exempt_accounts": "exempt_accounts.json",
"bootstrap_balances": "bootstrap_balances.json",
"historical_balance_enabled": true,
"reconciliation_disabled": false,
"inactive_discrepency_search_disabled": false,
"balance_tracking_disabled": false,
"end_conditions": {
"reconciliation_coverage": 0.95,
"index": 100
"reconciliation_coverage": {
"coverage": 0.80,
"index": 100
}
},
"results_output_file": "./rosetta-output/rosetta-cli-output.json"
},
"construction": {
"offline_url": "http://host.docker.internal:3999/rosetta/v1",
"currency": {
"symbol": "STX",
"decimals": 18
},
"minimum_balance": "0",
"maximum_fee": "10000000000000000",
"curve_type": "secp256k1",
"accounting_model": "account",
"scenario": [
"max_offline_connections": 0,
"stale_depth": 0,
"broadcast_limit": 0,
"ignore_broadcast_failures": false,
"clear_broadcasts": false,
"broadcast_behind_tip": false,
"block_broadcast_limit": 0,
"rebroadcast_all": false,
"constructor_dsl_file": "stacks.ros",
"prefunded_accounts": [
{
"operation_identifier": {
"index": 0
"privkey": "21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb7526",
"account_identifier": {
"address": "ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y"
},
"type": "NATIVE_TRANSFER",
"status": "",
"account": {
"address": "{{ SENDER }}"
},
"amount": {
"value": "{{ SENDER_VALUE }}",
"currency": {
"symbol": "STX",
"decimals": 6
}
"curve_type": "secp256k1",
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"operation_identifier": {
"index": 1
"privkey": "c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a",
"account_identifier": {
"address": "ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR"
},
"related_operations": [
{
"index": 0
}
],
"type": "NATIVE_TRANSFER",
"status": "",
"account": {
"address": "{{ RECIPIENT }}"
"curve_type": "secp256k1",
"currency": {
"symbol": "STX",
"decimals": 6
}
},
{
"privkey": "e75dcb66f84287eaf347955e94fa04337298dbd95aa0dbb985771104ef1913db",
"account_identifier": {
"address": "STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP"
},
"amount": {
"value": "{{ RECIPIENT_VALUE }}",
"currency": {
"symbol": "STX",
"decimals": 6
}
"curve_type": "secp256k1",
"currency": {
"symbol": "STX",
"decimals": 6
}
}
],
"confirmation_depth": 5,
"stale_depth": 30,
"broadcast_limit": 3,
"ignore_broadcast_failures": false,
"change_scenario": null,
"clear_broadcasts": false,
"broadcast_behind_tip": false,
"block_broadcast_limit": 5,
"rebroadcast_all": false,
"new_account_probability": 0.5,
"max_addresses": 200
"end_conditions": {
"create_account": 10,
"transfer": 10
},
"results_output_file": "./rosetta-output/rosetta-cli-output-const.json"
}
}

View File

@@ -5,13 +5,32 @@
},
"online_url": "http://localhost:3999/rosetta/v1",
"data_directory": "",
"http_timeout": 300,
"max_retries": 5,
"retry_elapsed_time": 0,
"max_online_connections": 0,
"max_sync_concurrency": 0,
"tip_delay": 60,
"log_configuration": false,
"http_timeout": 10,
"tip_delay": 300,
"data": {
"active_reconciliation_concurrency": 16,
"inactive_reconciliation_concurrency": 4,
"inactive_reconciliation_frequency": 250,
"log_blocks": true,
"log_transactions": true,
"log_balance_changes": true,
"log_reconciliations": false,
"ignore_reconciliation_error": false,
"exempt_accounts": "rosetta-cli-config/exempt_accounts.json",
"interesting_accounts": "interesting_accounts.json",
"historical_balance_enabled": true,
"reconciliation_disabled": false,
"inactive_discrepency_search_disabled": false,
"balance_tracking_disabled": false,
"end_conditions": {
"reconciliation_coverage": {
"coverage": 0.70,
"from_tip": true,
"index": 100
}
},
"results_output_file": "./rosetta-output/rosetta-cli-output.json"
},
"construction": {
"offline_url": "http://localhost:3999/rosetta/v1",
"max_offline_connections": 0,
@@ -23,8 +42,8 @@
"block_broadcast_limit": 0,
"rebroadcast_all": false,
"constructor_dsl_file": "stacks.ros",
"prefunded_accounts": [
{"privkey": "21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb7526",
"prefunded_accounts": [
{"privkey": "21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb7526",
"account_identifier": {"address": "ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y"},
"curve_type": "secp256k1",
"currency": {"symbol": "STX", "decimals": 6}},
@@ -39,6 +58,7 @@
"end_conditions": {
"create_account": 10,
"transfer": 10
}
},
"results_output_file": "./rosetta-output/rosetta-cli-output-const.json"
}
}

View File

@@ -6,7 +6,8 @@
"online_url": "http://localhost:3999/rosetta/v1",
"data_directory": "",
"http_timeout": 10,
"max_sync_concurrency": 8,
"sync_concurrency": 8,
"transaction_concurrency": 16,
"tip_delay": 300,
"data": {
"active_reconciliation_concurrency": 16,
@@ -18,13 +19,77 @@
"log_reconciliations": false,
"ignore_reconciliation_error": false,
"exempt_accounts": "rosetta-cli-config/exempt_accounts.json",
"bootstrap_balances": "bootstrap_balances.json",
"bootstrap_balances": "rosetta-cli-config/bootstrap_balances.json",
"historical_balance_disabled": true,
"interesting_accounts": "",
"reconciliation_disabled": true,
"reconciliation_disabled": false,
"inactive_discrepency_search_disabled": false,
"balance_tracking_disabled": false,
"end_conditions": {
"tip": true
"reconciliation_coverage": 0.4
}
},
"construction": {
"offline_url": "http://localhost:3999/rosetta/v1",
"currency": {
"symbol": "STX",
"decimals": 6
},
"minimum_balance": "0",
"maximum_fee": "10000000000000000",
"curve_type": "secp256k1",
"accounting_model": "account",
"scenario": [
{
"operation_identifier": {
"index": 0
},
"type": "NATIVE_TRANSFER",
"status": "",
"account": {
"address": "{{ SENDER }}"
},
"amount": {
"value": "{{ SENDER_VALUE }}",
"currency": {
"symbol": "STX",
"decimals": 6
}
}
},
{
"operation_identifier": {
"index": 1
},
"related_operations": [
{
"index": 0
}
],
"type": "NATIVE_TRANSFER",
"status": "",
"account": {
"address": "{{ RECIPIENT }}"
},
"amount": {
"value": "{{ RECIPIENT_VALUE }}",
"currency": {
"symbol": "STX",
"decimals": 6
}
}
}
],
"confirmation_depth": 5,
"stale_depth": 30,
"broadcast_limit": 3,
"ignore_broadcast_failures": false,
"change_scenario": null,
"clear_broadcasts": false,
"broadcast_behind_tip": false,
"block_broadcast_limit": 5,
"rebroadcast_all": false,
"new_account_probability": 0.5,
"max_addresses": 200
}
}

View File

@@ -294,7 +294,7 @@ function makeFeeOperation(tx: BaseTx): RosettaOperation {
const fee: RosettaOperation = {
operation_identifier: { index: 0 },
type: RosettaOperationType.Fee,
status: getTxStatus(DbTxStatus.Success),
status: getTxStatus(tx.status),
account: { address: tx.sender_address },
amount: {
value: (0n - unwrapOptional(tx.fee_rate, () => 'Unexpected nullish amount')).toString(10),
@@ -591,9 +591,9 @@ function parseStackingContractCall(
function parseGenericContractCall(operation: RosettaOperation, tx: BaseTx) {
operation.metadata = {
contract_call_function_name: tx.contract_call_function_name,
contract_call_function_args: bufferToHexPrefixString(
tx.contract_call_function_args ? tx.contract_call_function_args : Buffer.from('')
),
contract_call_function_args: tx.contract_call_function_args
? bufferToHexPrefixString(unwrapOptional(tx.contract_call_function_args, () => ''))
: '',
};
}

View File

@@ -0,0 +1,2 @@
CMD=/bin/rosetta-cli --configuration-file /app/rosetta-config-docker.json check:construction
OUTPUT=./rosetta-output-construction:/app/rosetta-output

View File

@@ -0,0 +1,23 @@
import { loadDotEnv } from '../helpers';
import { StacksCoreRpcClient } from '../core-rpc/client';
import { PgDataStore } from '../datastore/postgres-store';
export interface GlobalServices {
db: PgDataStore;
}
export default async (): Promise<void> => {
console.log('Jest - setup..');
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'test';
}
loadDotEnv();
const db = await PgDataStore.connect(true);
console.log('Waiting for RPC connection to core node..');
await new StacksCoreRpcClient().waitForConnection(60000);
const globalServices: GlobalServices = {
db: db,
};
Object.assign(global, globalServices);
console.log('Jest - setup done');
};

View File

@@ -0,0 +1,8 @@
import type { GlobalServices } from './setup';
export default async (): Promise<void> => {
console.log('Jest - teardown..');
const globalServices = (global as unknown) as GlobalServices;
await globalServices.db.close();
console.log('Jest - teardown done');
};

View File

@@ -0,0 +1,185 @@
import { PgDataStore, cycleMigrations, runMigrations } from '../datastore/postgres-store';
import { PoolClient } from 'pg';
import { ApiServer, startApiServer } from '../api/init';
import { startEventServer } from '../event-stream/event-server';
import { Server } from 'net';
import { DbBlock, DbMempoolTx, DbTx, DbTxStatus } from '../datastore/common';
import { AnchorMode, ChainID, makeSTXTokenTransfer } from '@stacks/transactions';
import { StacksTestnet } from '@stacks/network';
import * as BN from 'bn.js';
import * as fs from 'fs';
import { StacksCoreRpcClient, getCoreNodeEndpoint } from '../core-rpc/client';
import * as compose from 'docker-compose';
import * as path from 'path';
import Docker = require('dockerode');
const docker = new Docker();
const sender1 = {
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
privateKey: 'cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01',
};
const recipientAdd1 = 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y';
const HOST = 'localhost';
const PORT = 20443;
const stacksNetwork = GetStacksTestnetNetwork();
const isContainerRunning = async (name: string): Promise<boolean> =>
new Promise((resolve, reject): void => {
docker.listContainers((err: any, containers: any): void => {
if (err) {
reject(err);
}
const running = (containers || []).filter((container: any): boolean =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
container.Names.includes(name)
);
resolve(running.length > 0);
});
});
describe('Rosetta API', () => {
let db: PgDataStore;
let client: PoolClient;
let eventServer: Server;
let api: ApiServer;
let rosettaOutput: any;
beforeAll(async () => {
process.env.PG_DATABASE = 'postgres';
await cycleMigrations();
db = await PgDataStore.connect();
client = await db.pool.connect();
eventServer = await startEventServer({ datastore: db, chainId: ChainID.Testnet });
api = await startApiServer({ datastore: db, chainId: ChainID.Testnet });
// remove previous outputs if any
fs.rmdirSync('rosetta-output-construction', { recursive: true });
// build rosetta-cli container
await compose.buildOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: [
'-f',
'docker-compose.dev.rosetta-cli.yml',
'--env-file',
'src/tests-rosetta-cli-construction/envs/env.construction',
],
});
// start cli container
void compose.upOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: [
'-f',
'docker-compose.dev.rosetta-cli.yml',
'--env-file',
'src/tests-rosetta-cli-construction/envs/env.construction',
],
commandOptions: ['--abort-on-container-exit'],
});
await waitForBlock(api);
// await sleep(10000);
await transferStx(recipientAdd1, 1000000000, sender1.privateKey, api);
await transferStx(recipientAdd1, 1000000000, sender1.privateKey, api);
await transferStx(recipientAdd1, 1000000000, sender1.privateKey, api);
await transferStx(recipientAdd1, 1000000000, sender1.privateKey, api);
await transferStx(recipientAdd1, 1000000000, sender1.privateKey, api);
//wait on rosetta-cli to finish output
while (!rosettaOutput) {
if (fs.existsSync('rosetta-output-construction/rosetta-cli-output-const.json'))
rosettaOutput = require('../../rosetta-output-construction/rosetta-cli-output-const.json');
await sleep(1000);
}
});
it('check transaction confirmed', () => {
expect(rosettaOutput.stats.transactions_confirmed).toBeGreaterThan(1);
});
afterAll(async () => {
await new Promise(resolve => eventServer.close(() => resolve(true)));
await api.terminate();
client.release();
await db?.close();
await runMigrations(undefined, 'down');
});
});
async function transferStx(
recipientAddr: string,
amount: number,
senderPk: string,
api: ApiServer
) {
await waitForBlock(api);
const transferTx = await makeSTXTokenTransfer({
recipient: recipientAddr,
amount: new BN(amount),
senderKey: senderPk,
network: stacksNetwork,
memo: 'test-transaction',
sponsored: false,
anchorMode: AnchorMode.Any,
});
const serialized: Buffer = transferTx.serialize();
const { txId } = await sendCoreTx(serialized, api, 'transfer-stx');
await standByForTx(txId, api);
return txId;
}
function standByForTx(expectedTxId: string, api: ApiServer): Promise<string> {
const broadcastTx = new Promise<string>(resolve => {
const listener: (info: string) => void = info => {
api.datastore.removeListener('txUpdate', listener);
resolve(info);
};
api.datastore.addListener('txUpdate', listener);
});
return broadcastTx;
}
async function sendCoreTx(
serializedTx: Buffer,
api: ApiServer,
type: string
): Promise<{ txId: string }> {
try {
const submitResult = await new StacksCoreRpcClient({
host: HOST,
port: PORT,
}).sendTransaction(serializedTx);
return submitResult;
} catch (error) {
console.log(type);
console.error(error);
}
return Promise.resolve({ txId: '' });
}
export function GetStacksTestnetNetwork() {
const stacksNetwork = new StacksTestnet();
stacksNetwork.coreApiUrl = getCoreNodeEndpoint({
host: `http://${HOST}`,
port: PORT,
});
return stacksNetwork;
}
async function waitForBlock(api: ApiServer) {
await new Promise<string>(resolve => api.datastore.once('blockUpdate', block => resolve(block)));
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,5 @@
(define-public (say-hi)
(ok "hello world"))
(define-public (echo-number (val int))
(ok val))

View File

@@ -0,0 +1,2 @@
CMD=/bin/rosetta-cli --configuration-file /app/rosetta-config-docker.json check:data
OUTPUT=./rosetta-output:/app/rosetta-output

View File

@@ -0,0 +1,23 @@
import { loadDotEnv } from '../helpers';
import { StacksCoreRpcClient } from '../core-rpc/client';
import { PgDataStore } from '../datastore/postgres-store';
export interface GlobalServices {
db: PgDataStore;
}
export default async (): Promise<void> => {
console.log('Jest - setup..');
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'test';
}
loadDotEnv();
const db = await PgDataStore.connect(true);
console.log('Waiting for RPC connection to core node..');
await new StacksCoreRpcClient().waitForConnection(60000);
const globalServices: GlobalServices = {
db: db,
};
Object.assign(global, globalServices);
console.log('Jest - setup done');
};

View File

@@ -0,0 +1,8 @@
import type { GlobalServices } from './setup';
export default async (): Promise<void> => {
console.log('Jest - teardown..');
const globalServices = (global as unknown) as GlobalServices;
await globalServices.db.close();
console.log('Jest - teardown done');
};

View File

@@ -17,12 +17,13 @@ import {
ClarityAbi,
encodeClarityValue,
ChainID,
AnchorMode,
} from '@stacks/transactions';
import { StacksTestnet } from '@stacks/network';
import * as BN from 'bn.js';
import * as fs from 'fs';
import { StacksCoreRpcClient, getCoreNodeEndpoint } from '../core-rpc/client';
import { unwrapOptional } from '../helpers';
import { timeout, unwrapOptional } from '../helpers';
import * as compose from 'docker-compose';
import * as path from 'path';
import Docker = require('dockerode');
@@ -94,8 +95,6 @@ const contracts: string[] = [];
const HOST = 'localhost';
const PORT = 20443;
const URL = `http://${HOST}:${PORT}`;
const stacksNetwork = getStacksTestnetNetwork();
const isContainerRunning = async (name: string): Promise<boolean> =>
@@ -106,6 +105,7 @@ const isContainerRunning = async (name: string): Promise<boolean> =>
}
const running = (containers || []).filter((container: any): boolean =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
container.Names.includes(name)
);
@@ -135,13 +135,23 @@ describe('Rosetta API', () => {
await compose.buildOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: ['-f', 'docker-compose.dev.rosetta-cli.yml'],
composeOptions: [
'-f',
'docker-compose.dev.rosetta-cli.yml',
'--env-file',
'src/tests-rosetta-cli-data/envs/env.data',
],
});
// start cli container
void compose.upOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: ['-f', 'docker-compose.dev.rosetta-cli.yml'],
composeOptions: [
'-f',
'docker-compose.dev.rosetta-cli.yml',
'--env-file',
'src/tests-rosetta-cli-data/envs/env.data',
],
commandOptions: ['--abort-on-container-exit'],
});
@@ -156,7 +166,7 @@ describe('Rosetta API', () => {
for (const sender of senders) {
const response = await deployContract(
sender.privateKey,
'src/tests-rosetta-cli/contracts/hello-world.clar',
'src/tests-rosetta-cli-data/contracts/hello-world.clar',
api
);
contracts.push(response.contractId);
@@ -167,15 +177,12 @@ describe('Rosetta API', () => {
await callContractFunction(api, sender2.privateKey, contract, 'say-hi');
}
// wait for rosetta-cli to exit
let check = true;
while (check) {
// todo: remove hardcoded container name with dynamic
check = await isContainerRunning('/stacks-blockchain-api_rosetta-cli_1');
await sleep(2000);
//wait on rosetta-cli to finish output
while (!rosettaOutput) {
if (fs.existsSync('rosetta-output'))
rosettaOutput = require('../../rosetta-output/rosetta-cli-output.json');
await timeout(1000);
}
rosettaOutput = require('../../rosetta-output/rosetta-cli-output.json');
});
it('check request/response', () => {
@@ -239,6 +246,7 @@ async function callContractFunction(
network: stacksNetwork,
postConditionMode: PostConditionMode.Allow,
sponsored: false,
anchorMode: AnchorMode.Any,
});
const fee = await estimateContractFunctionCall(contractCallTx, stacksNetwork);
contractCallTx.setFee(fee);
@@ -262,6 +270,7 @@ async function deployContract(senderPk: string, sourceFile: string, api: ApiServ
network: stacksNetwork,
postConditionMode: PostConditionMode.Allow,
sponsored: false,
anchorMode: AnchorMode.Any,
});
const contractId = senderAddress + '.' + contractName;
@@ -290,6 +299,7 @@ async function transferStx(
network: stacksNetwork,
memo: 'test-transaction',
sponsored: false,
anchorMode: AnchorMode.Any,
});
const serialized: Buffer = transferTx.serialize();
@@ -330,9 +340,7 @@ function uniqueId() {
}
async function waitForBlock(api: ApiServer) {
await new Promise<string>(resolve => api.datastore.once('blockUpdate', blockHash => resolve(blockHash)));
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
await new Promise<string>(resolve =>
api.datastore.once('blockUpdate', blockHash => resolve(blockHash))
);
}

View File

@@ -1,285 +0,0 @@
import { PgDataStore, cycleMigrations, runMigrations } from '../datastore/postgres-store';
import { PoolClient } from 'pg';
import { ApiServer, startApiServer } from '../api/init';
import { startEventServer } from '../event-stream/event-server';
import { Server } from 'net';
import fetch from 'node-fetch';
import { DbBlock } from '../datastore/common';
import {
makeSTXTokenTransfer,
makeContractDeploy,
PostConditionMode,
makeContractCall,
ClarityValue,
getAddressFromPrivateKey,
getAbi,
estimateContractFunctionCall,
ClarityAbi,
encodeClarityValue,
ChainID,
} from '@stacks/transactions';
import { StacksTestnet } from '@stacks/network';
import * as BN from 'bn.js';
import * as fs from 'fs';
import { StacksCoreRpcClient, getCoreNodeEndpoint } from '../core-rpc/client';
import { unwrapOptional } from '../helpers';
import * as compose from 'docker-compose';
import * as path from 'path';
import Docker = require('dockerode');
const docker = new Docker();
const sender1 = {
address: 'STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2',
privateKey: 'ce109fee08860bb16337c76647dcbc02df0c06b455dd69bcf30af74d4eedd19301',
};
const sender2 = {
address: 'ST18MDW2PDTBSCR1ACXYRJP2JX70FWNM6YY2VX4SS',
privateKey: '08c14a1eada0dd42b667b40f59f7c8dedb12113613448dc04980aea20b268ddb01',
};
const sender3 = {
address: 'ST1DTAEAKM02GKCT4NGKTVER8MTJJHYQ9NT27E677',
privateKey: 'fdab825d3a12ca73d24c0b446eda8639605025450a4bf6716a2627121c594d0a01',
};
const senders = [sender1, sender2, sender3];
const recipientAdd1 = 'ST3KTZ45AQES4PNNHB2YGGJ4JXQQMRACRNZPQ19SP';
const recipientAdd2 = 'ST17ZNMSQMDARSCZ85Z7BVJX6T20ZWDF3VX0ZP33K';
const recipientAdd3 = 'ST3DR3THSKSWRH10SEDFFP3D90KZYG57J5N8M9KR9';
const recipientPk3 = '3af9b0b442389252c61db56233a2267cd242cc9f8a3284ae64808784c684c4ef01';
const recipientAdd4 = 'ST3WHD5WEKHENA38T9BWZK9M0SDXA46T9DQT0ZDTY';
const recipientAdd5 = 'ST2WZKZVZRJJMT2RJPWWM9FVC20S9R30CNVMJEX0H';
const recipientAdd6 = 'STFHWET10QDAQ8B88WNAVEV15XVNWNNCTMQGD5KN';
const recipientAdd7 = 'ST3YFNTA33A14NYTE0P85790E2X7Y890SD24K8WAK';
const recipientAdd8 = 'ST367HN911AFQ666829M2C6XP0HPMSTT517B5FMNB';
const recipientAdd9 = 'STN5W5SAFHVWS9P6BDMVJQZWQGCAHQBPXQYKKCCW';
const recipientAdd10 = 'ST5WDT0C4995DWA3H7YDKYM0NK4QNADR4T93HYD1';
const recipientAdd11 = 'ST1JBBKAPX6XDGR9TEGQTNVV2H4NJX456FKRJ5QWA';
const recipientAdd12 = 'ST3T6ERPJ2TXPQ668747Z52P4EJBYY966SVFM7W8X';
const recipientAdd13 = 'ST2Y60MJ5CPGEN1CP405H7X8CPCEYV2FJ0PHZ742P';
const recipientAdd14 = 'STAXJZ26VDBNA5TVJRJ63SV77C1AWW786X84V634';
const recipientAdd15 = 'ST3Q362NAVV3W6T1GV0XH78JJJ0BKZREHR6C168TV';
const recipientAdd16 = 'ST16EDNWEW80AX2737KA58SJZQ8QNW7DXKTDBBVCV';
const recipientAdd17 = 'ST3VHZ1ETEB0D7V27TCB5BB9YVQVWPQJHJG5Y6ARH';
const recipientAdd18 = 'ST2NC3GSVB511TPCBDKB4PRA6VJHYNSGH93756H1M';
const recipientAdd19 = 'ST11QXGG0SRPC5QYESX3WA55BKQWWMPV7CSWKG6SN';
const recipientAdd20 = 'STJ5PW80NKC6NJ0J87YWXYNGE474RN38K2B4YW1R';
const recipients = [
recipientAdd1,
recipientAdd2,
recipientAdd3,
recipientAdd4,
recipientAdd5,
recipientAdd6,
recipientAdd7,
recipientAdd8,
recipientAdd9,
recipientAdd10,
recipientAdd11,
recipientAdd12,
recipientAdd13,
recipientAdd14,
recipientAdd15,
recipientAdd16,
recipientAdd17,
recipientAdd18,
recipientAdd19,
recipientAdd20,
];
const contracts: string[] = [];
const HOST = 'localhost';
const PORT = 20443;
const URL = `http://${HOST}:${PORT}`;
const stacksNetwork = getStacksTestnetNetwork();
const isContainerRunning = async (name: string): Promise<boolean> =>
new Promise((resolve, reject): void => {
docker.listContainers((err: any, containers: any): void => {
if (err) {
reject(err);
}
const running = (containers || []).filter((container: any): boolean =>
container.Names.includes(name)
);
resolve(running.length > 0);
});
});
describe('Rosetta API', () => {
let db: PgDataStore;
let client: PoolClient;
let eventServer: Server;
let api: ApiServer;
let rosettaOutput: any;
beforeAll(async () => {
process.env.PG_DATABASE = 'postgres';
await cycleMigrations();
db = await PgDataStore.connect();
client = await db.pool.connect();
eventServer = await startEventServer({ datastore: db, chainId: ChainID.Testnet });
api = await startApiServer({ datastore: db, chainId: ChainID.Testnet });
// build rosetta-cli container
await compose.buildOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: ['-f', 'docker-compose.dev.rosetta-construction.yml'],
});
// start cli container
void compose.upOne('rosetta-cli', {
cwd: path.join(__dirname, '../../'),
log: true,
composeOptions: ['-f', 'docker-compose.dev.rosetta-construction.yml'],
commandOptions: ['--abort-on-container-exit'],
});
await waitForBlock(api);
});
afterAll(async () => {
await new Promise<void>(resolve => eventServer.close(() => resolve()));
await api.terminate();
client.release();
await db?.close();
await runMigrations(undefined, 'down');
});
});
async function callContractFunction(
api: ApiServer,
senderPk: string,
contractId: string,
functionName: string,
...functionArgs: string[]
) {
await waitForBlock(api);
const [contractAddr, contractName] = contractId.split('.');
const contractAbi: ClarityAbi = await getAbi(contractAddr, contractName, stacksNetwork);
const abiFunction = contractAbi.functions.find(fn => fn.name === functionName);
if (abiFunction === undefined) {
throw new Error(`Contract ${contractId} ABI does not have function "${functionName}"`);
}
const clarityValueArgs: ClarityValue[] = new Array(abiFunction.args.length);
for (let i = 0; i < clarityValueArgs.length; i++) {
const abiArg = abiFunction.args[i];
const stringArg = unwrapOptional(functionArgs[i]);
const clarityVal = encodeClarityValue(abiArg.type, stringArg);
clarityValueArgs[i] = clarityVal;
}
const contractCallTx = await makeContractCall({
contractAddress: contractAddr,
contractName: contractName,
functionName: functionName,
functionArgs: clarityValueArgs,
senderKey: senderPk,
network: stacksNetwork,
postConditionMode: PostConditionMode.Allow,
sponsored: false,
});
const fee = await estimateContractFunctionCall(contractCallTx, stacksNetwork);
contractCallTx.setFee(fee);
const serialized: Buffer = contractCallTx.serialize();
const { txId } = await sendCoreTx(serialized, api, 'call-contract-func');
}
async function deployContract(senderPk: string, sourceFile: string, api: ApiServer) {
await waitForBlock(api);
const contractName = `test-contract-${uniqueId()}`;
const senderAddress = getAddressFromPrivateKey(senderPk, stacksNetwork.version);
const source = fs.readFileSync(sourceFile).toString();
const normalized_contract_source = source.replace(/\r/g, '').replace(/\t/g, ' ');
const contractDeployTx = await makeContractDeploy({
contractName: contractName,
codeBody: normalized_contract_source,
senderKey: senderPk,
network: stacksNetwork,
postConditionMode: PostConditionMode.Allow,
sponsored: false,
});
const contractId = senderAddress + '.' + contractName;
const feeRateReq = await fetch(stacksNetwork.getTransferFeeEstimateApiUrl());
const feeRateResult = await feeRateReq.text();
const txBytes = new BN(contractDeployTx.serialize().byteLength);
const feeRate = new BN(feeRateResult);
const fee = feeRate.mul(txBytes);
contractDeployTx.setFee(fee);
const { txId } = await sendCoreTx(contractDeployTx.serialize(), api, 'deploy-contract');
return { txId, contractId };
}
async function transferStx(
recipientAddr: string,
amount: number,
senderPk: string,
api: ApiServer
) {
await waitForBlock(api);
const transferTx = await makeSTXTokenTransfer({
recipient: recipientAddr,
amount: new BN(amount),
senderKey: senderPk,
network: stacksNetwork,
memo: 'test-transaction',
sponsored: false,
});
const serialized: Buffer = transferTx.serialize();
const { txId } = await sendCoreTx(serialized, api, 'transfer-stx');
return txId;
}
async function sendCoreTx(
serializedTx: Buffer,
api: ApiServer,
type: string
): Promise<{ txId: string }> {
try {
const submitResult = await new StacksCoreRpcClient({
host: HOST,
port: PORT,
}).sendTransaction(serializedTx);
return submitResult;
} catch (error) {
console.log(type);
console.error(error);
}
return Promise.resolve({ txId: '' });
}
export function getStacksTestnetNetwork() {
const stacksNetwork = new StacksTestnet();
stacksNetwork.coreApiUrl = getCoreNodeEndpoint({
host: `http://${HOST}`,
port: PORT,
});
return stacksNetwork;
}
function uniqueId() {
return Math.random().toString(16).slice(-4);
}
async function waitForBlock(api: ApiServer) {
await new Promise<string>(resolve => api.datastore.once('blockUpdate', blockHash => resolve(blockHash)));
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}