mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-13 08:40:42 +08:00
feat: support for subnets (#1549)
* test: initial integration test for subnets * test: ensure first subnet block is mined * feat: synthetic nft deposit tx * test: increase timeout for subnets test * test: attempt fix test flakiness in CI * test: much faster subnet block mining time, subnet tests 5-10x faster * ci: bump subnet-node docker image * test: complete withdrawal on stacks chain * test: try waiting for subnet contract interface to be ready * test: verify NFT ownership on L1 after withdrawal * test: fix subnet node port env vars * feat: implement parsing for FT deposit synthetic tx * chore: rename test file * chore: do not skip test * test: progress on subnet STX use-case test * feat: implement parsing for STX deposit synthetic tx * test: validation of synthetic tx for L1 deposit-stx * test: validation of synthetic tx for L1 deposit-nft-asset * test: validation of synthetic tx for L1 deposit-ft-asset * chore: lint fix * chore: remove debug comments
This commit is contained in:
59
.github/workflows/ci.yml
vendored
59
.github/workflows/ci.yml
vendored
@@ -401,6 +401,65 @@ jobs:
|
||||
flag-name: run-${{ github.job }}
|
||||
parallel: true
|
||||
|
||||
test-subnets:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- 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 ci --audit=false
|
||||
|
||||
- name: Setup env vars
|
||||
run: echo "STACKS_CORE_EVENT_HOST=http://0.0.0.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup integration environment
|
||||
run: |
|
||||
sudo ufw disable
|
||||
npm run devenv:deploy:subnets -- -d
|
||||
npm run devenv:logs:subnets -- --no-color &> docker-compose-logs.txt &
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:subnets
|
||||
|
||||
- name: Print integration environment logs
|
||||
run: cat docker-compose-logs.txt
|
||||
if: failure()
|
||||
|
||||
- name: Teardown integration environment
|
||||
run: npm run devenv:stop:subnets
|
||||
if: always()
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
if: always()
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
if: always()
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
flag-name: run-${{ github.job }}
|
||||
parallel: true
|
||||
|
||||
test-2_1:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -229,6 +229,25 @@
|
||||
"preLaunchTask": "stacks-node:deploy-dev",
|
||||
"postDebugTask": "stacks-node:stop-dev"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest: Subnets",
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"args": [
|
||||
"--testTimeout=3600000",
|
||||
"--runInBand",
|
||||
"--no-cache",
|
||||
"--config",
|
||||
"${workspaceRoot}/tests/jest.config.subnets.js"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"console": "integratedTerminal",
|
||||
"preLaunchTask": "deploy:subnets",
|
||||
"postDebugTask": "stop:subnets",
|
||||
"smartStep": false,
|
||||
"sourceMaps": true,
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
||||
23
.vscode/tasks.json
vendored
23
.vscode/tasks.json
vendored
@@ -22,6 +22,29 @@
|
||||
},
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "dedicated", "clear": false }
|
||||
},
|
||||
{
|
||||
"label": "deploy:subnets",
|
||||
"type": "shell",
|
||||
"command": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.subnets.yml up --force-recreate -V",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"pattern": { "regexp": ".", "file": 1, "location": 2, "message": 3 },
|
||||
"background": { "activeOnStart": true, "beginsPattern": ".", "endsPattern": "." }
|
||||
},
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "dedicated", "clear": false }
|
||||
},
|
||||
{
|
||||
"label": "stop:subnets",
|
||||
"type": "shell",
|
||||
"command": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.subnets.yml down -v -t 0",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "deploy:krypton",
|
||||
"type": "shell",
|
||||
|
||||
36
docker/docker-compose.dev.subnets.yml
Normal file
36
docker/docker-compose.dev.subnets.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
version: '3.7'
|
||||
services:
|
||||
stacks-blockchain:
|
||||
image: "hirosystems/stacks-api-e2e:stacks2.1-ecb1872"
|
||||
command: |
|
||||
bash -c "rm /event-log.ndjson && /root/run.sh"
|
||||
ports:
|
||||
- "18443:18443" # bitcoin regtest JSON-RPC interface
|
||||
- "18444:18444" # bitcoin regtest p2p
|
||||
- "20443:20443" # stacks-node RPC interface
|
||||
- "20444:20444" # stacks-node p2p
|
||||
environment:
|
||||
MINE_INTERVAL: 1s
|
||||
STACKS_EVENT_OBSERVER: host.docker.internal:30445
|
||||
# STACKS_LOG_TRACE: 1
|
||||
# STACKS_LOG_DEBUG: 1
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # fixes `host.docker.internal` on linux hosts
|
||||
stacks-subnet:
|
||||
# restart: on-failure
|
||||
#image: "hirosystems/stacks-subnets:206-merge-stretch"
|
||||
image: "hirosystems/stacks-subnets@sha256:7e296cd319f81b87b5a9c11f4c478dafb122c114dc805d832d68f39be150af49"
|
||||
command: subnet-node start --config=/app/config/Stacks-subnet.toml
|
||||
ports:
|
||||
- "30443:30443" # subnet-node RPC interface
|
||||
- "30444:30444" # subnet-node p2p
|
||||
- "30445:30445" # subnet-node event-observer
|
||||
environment:
|
||||
STACKS_EVENT_OBSERVER: host.docker.internal:3700
|
||||
# STACKS_LOG_TRACE: 1
|
||||
# STACKS_LOG_DEBUG: 1
|
||||
volumes:
|
||||
- ../stacks-blockchain/:/app/config
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # fixes `host.docker.internal` on linux hosts
|
||||
@@ -8,6 +8,7 @@
|
||||
"dev:integrated": "npm run devenv:build && concurrently npm:dev npm:devenv:deploy",
|
||||
"dev:follower": "npm run devenv:build && concurrently npm:dev npm:devenv:follower",
|
||||
"test": "cross-env NODE_ENV=development jest --config ./tests/jest.config.js --coverage --runInBand",
|
||||
"test:subnets": "cross-env NODE_ENV=development jest --config ./tests/jest.config.subnets.js --coverage --runInBand",
|
||||
"test:2.1": "cross-env NODE_ENV=development jest --config ./tests/jest.config.2.1.js --coverage --runInBand",
|
||||
"test:2.1-transition": "cross-env NODE_ENV=development jest --config ./tests/jest.config.2.1-transition.js --coverage --runInBand",
|
||||
"test:rosetta": "cross-env NODE_ENV=development jest --config ./tests/jest.config.rosetta.js --coverage --runInBand",
|
||||
@@ -20,6 +21,7 @@
|
||||
"test:tokens": "cross-env NODE_ENV=development jest --config ./tests/jest.config.tokens.js --coverage --runInBand",
|
||||
"test:watch": "cross-env NODE_ENV=development jest --config ./tests/jest.config.js --watch",
|
||||
"test:integration": "concurrently \"docker compose -f docker/docker-compose.dev.postgres.yml up --force-recreate -V\" \"cross-env NODE_ENV=development jest --config ./tests/jest.config.js --no-cache --runInBand; npm run devenv:stop:pg\"",
|
||||
"test:integration:subnets": "concurrently --hide \"devenv:deploy:subnets\" \"npm:devenv:deploy:subnets\" \"cross-env NODE_ENV=development jest --config ./tests/jest.config.subnets.js --no-cache --runInBand; npm run devenv:stop:subnets\"",
|
||||
"test:integration:2.1": "concurrently --hide \"devenv:deploy:2.1\" \"npm:devenv:deploy:2.1\" \"cross-env NODE_ENV=development jest --config ./tests/jest.config.2.1.js --no-cache --runInBand; npm run devenv:stop:2.1\"",
|
||||
"test:integration:2.1-transition": "concurrently --hide \"devenv:deploy:2.1-transition\" \"npm:devenv:deploy:2.1-transition\" \"cross-env NODE_ENV=development jest --config ./tests/jest.config.2.1-transition.js --no-cache --runInBand; npm run devenv:stop:2.1-transition\"",
|
||||
"test:integration:rosetta": "concurrently \"docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml up --force-recreate -V\" \"cross-env NODE_ENV=development jest --config ./tests/jest.config.rosetta.js --no-cache --runInBand; npm run devenv:stop\"",
|
||||
@@ -45,12 +47,15 @@
|
||||
"devenv:deploy": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml -f docker/docker-compose.dev.bitcoind.yml up --force-recreate -V",
|
||||
"devenv:follower": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain-follower.yml up --force-recreate -V",
|
||||
"devenv:stop": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml -f docker/docker-compose.dev.bitcoind.yml down -v -t 0",
|
||||
"devenv:deploy:subnets": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.subnets.yml up --force-recreate -V",
|
||||
"devenv:stop:subnets": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.subnets.yml down -v -t 0",
|
||||
"devenv:deploy:2.1": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton.yml up --force-recreate -V",
|
||||
"devenv:deploy:2.1-transition": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton-2.1-transition.yml up --force-recreate -V",
|
||||
"devenv:stop:2.1": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton.yml down -v -t 0",
|
||||
"devenv:stop:2.1-transition": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton-2.1-transition.yml down -v -t 0",
|
||||
"devenv:stop:pg": "docker compose -f docker/docker-compose.dev.postgres.yml down -v -t 0",
|
||||
"devenv:logs": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml -f docker/docker-compose.dev.bitcoind.yml logs -t -f",
|
||||
"devenv:logs:subnets": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.subnets.yml logs -t -f",
|
||||
"devenv:logs:2.1": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton.yml logs -t -f",
|
||||
"devenv:logs:2.1-transition": "docker compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-krypton-2.1-transition.yml logs -t -f"
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface StxTransferEvent extends CoreNodeEventBase {
|
||||
};
|
||||
}
|
||||
|
||||
interface StxMintEvent extends CoreNodeEventBase {
|
||||
export interface StxMintEvent extends CoreNodeEventBase {
|
||||
type: CoreNodeEventType.StxMintEvent;
|
||||
stx_mint_event: {
|
||||
recipient: string;
|
||||
@@ -93,7 +93,7 @@ export interface NftTransferEvent extends CoreNodeEventBase {
|
||||
};
|
||||
}
|
||||
|
||||
interface NftMintEvent extends CoreNodeEventBase {
|
||||
export interface NftMintEvent extends CoreNodeEventBase {
|
||||
type: CoreNodeEventType.NftMintEvent;
|
||||
nft_mint_event: {
|
||||
/** Fully qualified asset ID, e.g. "ST2ZRX0K27GW0SP3GJCEMHD95TQGJMKB7G9Y0X1MH.contract-name.asset-name" */
|
||||
@@ -128,7 +128,7 @@ interface FtTransferEvent extends CoreNodeEventBase {
|
||||
};
|
||||
}
|
||||
|
||||
interface FtMintEvent extends CoreNodeEventBase {
|
||||
export interface FtMintEvent extends CoreNodeEventBase {
|
||||
type: CoreNodeEventType.FtMintEvent;
|
||||
ft_mint_event: {
|
||||
/** Fully qualified asset ID, e.g. "ST2ZRX0K27GW0SP3GJCEMHD95TQGJMKB7G9Y0X1MH.contract-name.asset-name" */
|
||||
|
||||
@@ -5,9 +5,12 @@ import {
|
||||
CoreNodeMicroblockTxMessage,
|
||||
CoreNodeParsedTxMessage,
|
||||
CoreNodeTxMessage,
|
||||
FtMintEvent,
|
||||
isTxWithMicroblockInfo,
|
||||
NftMintEvent,
|
||||
SmartContractEvent,
|
||||
StxLockEvent,
|
||||
StxMintEvent,
|
||||
StxTransferEvent,
|
||||
} from './core-node-message';
|
||||
import {
|
||||
@@ -58,6 +61,8 @@ import {
|
||||
SomeCV,
|
||||
NoneCV,
|
||||
UIntCV,
|
||||
stringAsciiCV,
|
||||
hexToCV,
|
||||
} from '@stacks/transactions';
|
||||
import { poxAddressToTuple } from '@stacks/stacking';
|
||||
import { c32ToB58 } from 'c32check';
|
||||
@@ -78,6 +83,172 @@ export function getTxSponsorAddress(tx: DecodedTxResult): string | undefined {
|
||||
return sponsorAddress;
|
||||
}
|
||||
|
||||
function createSubnetTransactionFromL1NftDeposit(
|
||||
chainId: ChainID,
|
||||
event: NftMintEvent,
|
||||
txId: string
|
||||
): DecodedTxResult {
|
||||
const decRecipientAddress = decodeStacksAddress(event.nft_mint_event.recipient);
|
||||
const [contractAddress, contractName] = event.nft_mint_event.asset_identifier
|
||||
.split('::')[0]
|
||||
.split('.');
|
||||
const decContractAddress = decodeStacksAddress(contractAddress);
|
||||
const legacyClarityVals = [
|
||||
hexToCV(event.nft_mint_event.raw_value),
|
||||
principalCV(event.nft_mint_event.recipient),
|
||||
];
|
||||
const fnLenBuffer = Buffer.alloc(4);
|
||||
fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
|
||||
const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
|
||||
const rawFnArgs = bufferToHexPrefixString(
|
||||
Buffer.concat([fnLenBuffer, ...serializedClarityValues])
|
||||
);
|
||||
const clarityFnArgs = decodeClarityValueList(rawFnArgs);
|
||||
|
||||
const tx: DecodedTxResult = {
|
||||
tx_id: txId,
|
||||
version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
|
||||
chain_id: chainId,
|
||||
auth: {
|
||||
type_id: PostConditionAuthFlag.Standard,
|
||||
origin_condition: {
|
||||
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
|
||||
signer: {
|
||||
address_version: decRecipientAddress[0],
|
||||
address_hash_bytes: decRecipientAddress[1],
|
||||
address: event.nft_mint_event.recipient,
|
||||
},
|
||||
nonce: '0',
|
||||
tx_fee: '0',
|
||||
key_encoding: TxPublicKeyEncoding.Compressed,
|
||||
signature: '0x',
|
||||
},
|
||||
},
|
||||
anchor_mode: AnchorModeID.Any,
|
||||
post_condition_mode: PostConditionModeID.Allow,
|
||||
post_conditions: [],
|
||||
post_conditions_buffer: '0x0100000000',
|
||||
payload: {
|
||||
type_id: TxPayloadTypeID.ContractCall,
|
||||
address_version: decContractAddress[0],
|
||||
address_hash_bytes: decContractAddress[1],
|
||||
address: contractAddress,
|
||||
contract_name: contractName,
|
||||
function_name: 'deposit-from-burnchain',
|
||||
function_args: clarityFnArgs,
|
||||
function_args_buffer: rawFnArgs,
|
||||
},
|
||||
};
|
||||
return tx;
|
||||
}
|
||||
|
||||
function createSubnetTransactionFromL1FtDeposit(
|
||||
chainId: ChainID,
|
||||
event: FtMintEvent,
|
||||
txId: string
|
||||
): DecodedTxResult {
|
||||
const decRecipientAddress = decodeStacksAddress(event.ft_mint_event.recipient);
|
||||
const [contractAddress, contractName] = event.ft_mint_event.asset_identifier
|
||||
.split('::')[0]
|
||||
.split('.');
|
||||
const decContractAddress = decodeStacksAddress(contractAddress);
|
||||
const legacyClarityVals = [
|
||||
uintCV(event.ft_mint_event.amount),
|
||||
principalCV(event.ft_mint_event.recipient),
|
||||
];
|
||||
const fnLenBuffer = Buffer.alloc(4);
|
||||
fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
|
||||
const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
|
||||
const rawFnArgs = bufferToHexPrefixString(
|
||||
Buffer.concat([fnLenBuffer, ...serializedClarityValues])
|
||||
);
|
||||
const clarityFnArgs = decodeClarityValueList(rawFnArgs);
|
||||
|
||||
const tx: DecodedTxResult = {
|
||||
tx_id: txId,
|
||||
version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
|
||||
chain_id: chainId,
|
||||
auth: {
|
||||
type_id: PostConditionAuthFlag.Standard,
|
||||
origin_condition: {
|
||||
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
|
||||
signer: {
|
||||
address_version: decRecipientAddress[0],
|
||||
address_hash_bytes: decRecipientAddress[1],
|
||||
address: event.ft_mint_event.recipient,
|
||||
},
|
||||
nonce: '0',
|
||||
tx_fee: '0',
|
||||
key_encoding: TxPublicKeyEncoding.Compressed,
|
||||
signature: '0x',
|
||||
},
|
||||
},
|
||||
anchor_mode: AnchorModeID.Any,
|
||||
post_condition_mode: PostConditionModeID.Allow,
|
||||
post_conditions: [],
|
||||
post_conditions_buffer: '0x0100000000',
|
||||
payload: {
|
||||
type_id: TxPayloadTypeID.ContractCall,
|
||||
address_version: decContractAddress[0],
|
||||
address_hash_bytes: decContractAddress[1],
|
||||
address: contractAddress,
|
||||
contract_name: contractName,
|
||||
function_name: 'deposit-from-burnchain',
|
||||
function_args: clarityFnArgs,
|
||||
function_args_buffer: rawFnArgs,
|
||||
},
|
||||
};
|
||||
return tx;
|
||||
}
|
||||
|
||||
function createSubnetTransactionFromL1StxDeposit(
|
||||
chainId: ChainID,
|
||||
event: StxMintEvent,
|
||||
txId: string
|
||||
): DecodedTxResult {
|
||||
const recipientAddress = decodeStacksAddress(event.stx_mint_event.recipient);
|
||||
const bootAddressString =
|
||||
chainId === ChainID.Mainnet ? 'SP000000000000000000002Q6VF78' : 'ST000000000000000000002AMW42H';
|
||||
const bootAddress = decodeStacksAddress(bootAddressString);
|
||||
|
||||
const tx: DecodedTxResult = {
|
||||
tx_id: txId,
|
||||
version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
|
||||
chain_id: chainId,
|
||||
auth: {
|
||||
type_id: PostConditionAuthFlag.Standard,
|
||||
origin_condition: {
|
||||
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
|
||||
signer: {
|
||||
address_version: bootAddress[0],
|
||||
address_hash_bytes: bootAddress[1],
|
||||
address: bootAddressString,
|
||||
},
|
||||
nonce: '0',
|
||||
tx_fee: '0',
|
||||
key_encoding: TxPublicKeyEncoding.Compressed,
|
||||
signature: '0x',
|
||||
},
|
||||
},
|
||||
anchor_mode: AnchorModeID.Any,
|
||||
post_condition_mode: PostConditionModeID.Allow,
|
||||
post_conditions: [],
|
||||
post_conditions_buffer: '0x0100000000',
|
||||
payload: {
|
||||
type_id: TxPayloadTypeID.TokenTransfer,
|
||||
recipient: {
|
||||
type_id: PrincipalTypeID.Standard,
|
||||
address_version: recipientAddress[0],
|
||||
address_hash_bytes: recipientAddress[1],
|
||||
address: event.stx_mint_event.recipient,
|
||||
},
|
||||
amount: BigInt(event.stx_mint_event.amount).toString(),
|
||||
memo_hex: '0x',
|
||||
},
|
||||
};
|
||||
return tx;
|
||||
}
|
||||
|
||||
function createTransactionFromCoreBtcStxLockEvent(
|
||||
chainId: ChainID,
|
||||
event: StxLockEvent,
|
||||
@@ -372,6 +543,15 @@ export function parseMessageTransaction(
|
||||
const stxLockEvent = events.find(
|
||||
(e): e is StxLockEvent => e.type === CoreNodeEventType.StxLockEvent
|
||||
);
|
||||
const nftMintEvent = events.find(
|
||||
(e): e is NftMintEvent => e.type === CoreNodeEventType.NftMintEvent
|
||||
);
|
||||
const ftMintEvent = events.find(
|
||||
(e): e is FtMintEvent => e.type === CoreNodeEventType.FtMintEvent
|
||||
);
|
||||
const stxMintEvent = events.find(
|
||||
(e): e is StxMintEvent => e.type === CoreNodeEventType.StxMintEvent
|
||||
);
|
||||
|
||||
const pox2Event = events
|
||||
.filter(
|
||||
@@ -419,11 +599,20 @@ export function parseMessageTransaction(
|
||||
coreTx.txid
|
||||
);
|
||||
txSender = pox2Event.decodedEvent.stacker;
|
||||
} else if (nftMintEvent) {
|
||||
rawTx = createSubnetTransactionFromL1NftDeposit(chainId, nftMintEvent, coreTx.txid);
|
||||
txSender = nftMintEvent.nft_mint_event.recipient;
|
||||
} else if (ftMintEvent) {
|
||||
rawTx = createSubnetTransactionFromL1FtDeposit(chainId, ftMintEvent, coreTx.txid);
|
||||
txSender = ftMintEvent.ft_mint_event.recipient;
|
||||
} else if (stxMintEvent) {
|
||||
rawTx = createSubnetTransactionFromL1StxDeposit(chainId, stxMintEvent, coreTx.txid);
|
||||
txSender = getTxSenderAddress(rawTx);
|
||||
} else {
|
||||
logError(
|
||||
`BTC transaction found, but no STX transfer event available to recreate transaction. TX: ${JSON.stringify(
|
||||
coreTx
|
||||
)}`
|
||||
)}, event: ${JSON.stringify(events)}`
|
||||
);
|
||||
throw new Error('Unable to generate transaction from BTC tx');
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ export async function standByForPoxCycleEnd(): Promise<CoreRpcPoxInfo> {
|
||||
}
|
||||
|
||||
export async function standByUntilBurnBlock(burnBlockHeight: number): Promise<DbBlock> {
|
||||
let blockFound = false;
|
||||
const dbBlock = await new Promise<DbBlock>(async resolve => {
|
||||
const listener: (blockHash: string) => void = async blockHash => {
|
||||
const dbBlockQuery = await testEnv.api.datastore.getBlock({ hash: blockHash });
|
||||
@@ -187,21 +188,27 @@ export async function standByUntilBurnBlock(burnBlockHeight: number): Promise<Db
|
||||
return;
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
blockFound = true;
|
||||
resolve(dbBlockQuery.result);
|
||||
};
|
||||
testEnv.api.datastore.eventEmitter.addListener('blockUpdate', listener);
|
||||
|
||||
// Check if block height already reached
|
||||
const curHeight = await testEnv.api.datastore.getCurrentBlock();
|
||||
if (curHeight.found && curHeight.result.burn_block_height >= burnBlockHeight) {
|
||||
const dbBlock = await testEnv.api.datastore.getBlock({
|
||||
height: curHeight.result.block_height,
|
||||
});
|
||||
if (!dbBlock.found) {
|
||||
throw new Error('Unhandled missing block');
|
||||
while (!blockFound) {
|
||||
const curHeight = await testEnv.api.datastore.getCurrentBlock();
|
||||
if (curHeight.found && curHeight.result.burn_block_height >= burnBlockHeight) {
|
||||
const dbBlock = await testEnv.api.datastore.getBlock({
|
||||
height: curHeight.result.block_height,
|
||||
});
|
||||
if (!dbBlock.found) {
|
||||
throw new Error('Unhandled missing block');
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
blockFound = true;
|
||||
resolve(dbBlock.result);
|
||||
} else {
|
||||
await timeout(200);
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
resolve(dbBlock.result);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -218,6 +225,7 @@ export async function standByUntilBurnBlock(burnBlockHeight: number): Promise<Db
|
||||
}
|
||||
|
||||
export async function standByForTx(expectedTxId: string): Promise<DbTx> {
|
||||
let txFound = false;
|
||||
const tx = await new Promise<DbTx>(async resolve => {
|
||||
const listener: (txId: string) => void = async txId => {
|
||||
if (txId !== expectedTxId) {
|
||||
@@ -231,18 +239,24 @@ export async function standByForTx(expectedTxId: string): Promise<DbTx> {
|
||||
return;
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('txUpdate', listener);
|
||||
txFound = true;
|
||||
resolve(dbTxQuery.result);
|
||||
};
|
||||
testEnv.api.datastore.eventEmitter.addListener('txUpdate', listener);
|
||||
|
||||
// Check if tx is already received
|
||||
const dbTxQuery = await testEnv.api.datastore.getTx({
|
||||
txId: expectedTxId,
|
||||
includeUnanchored: false,
|
||||
});
|
||||
if (dbTxQuery.found) {
|
||||
testEnv.api.datastore.eventEmitter.removeListener('txUpdate', listener);
|
||||
resolve(dbTxQuery.result);
|
||||
while (!txFound) {
|
||||
// Check if tx is already received
|
||||
const dbTxQuery = await testEnv.api.datastore.getTx({
|
||||
txId: expectedTxId,
|
||||
includeUnanchored: false,
|
||||
});
|
||||
if (dbTxQuery.found) {
|
||||
testEnv.api.datastore.eventEmitter.removeListener('txUpdate', listener);
|
||||
txFound = true;
|
||||
resolve(dbTxQuery.result);
|
||||
} else {
|
||||
await timeout(200);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -269,6 +283,7 @@ export async function standByForTxSuccess(expectedTxId: string): Promise<DbTx> {
|
||||
}
|
||||
|
||||
export async function standByUntilBlock(blockHeight: number): Promise<DbBlock> {
|
||||
let blockFound = false;
|
||||
const dbBlock = await new Promise<DbBlock>(async resolve => {
|
||||
const listener: (blockHash: string) => void = async blockHash => {
|
||||
const dbBlockQuery = await testEnv.api.datastore.getBlock({ hash: blockHash });
|
||||
@@ -276,20 +291,26 @@ export async function standByUntilBlock(blockHeight: number): Promise<DbBlock> {
|
||||
return;
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
blockFound = true;
|
||||
resolve(dbBlockQuery.result);
|
||||
};
|
||||
testEnv.api.datastore.eventEmitter.addListener('blockUpdate', listener);
|
||||
|
||||
// Check if block height already reached
|
||||
const curHeight = await testEnv.api.datastore.getCurrentBlockHeight();
|
||||
if (curHeight.found && curHeight.result >= blockHeight) {
|
||||
const dbBlock = await testEnv.api.datastore.getBlock({ height: curHeight.result });
|
||||
if (!dbBlock.found) {
|
||||
throw new Error('Unhandled missing block');
|
||||
while (!blockFound) {
|
||||
const curHeight = await testEnv.api.datastore.getCurrentBlockHeight();
|
||||
if (curHeight.found && curHeight.result >= blockHeight) {
|
||||
const dbBlock = await testEnv.api.datastore.getBlock({ height: curHeight.result });
|
||||
if (!dbBlock.found) {
|
||||
throw new Error('Unhandled missing block');
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
blockFound = true;
|
||||
resolve(dbBlock.result);
|
||||
return;
|
||||
} else {
|
||||
await timeout(200);
|
||||
}
|
||||
testEnv.api.datastore.eventEmitter.removeListener('blockUpdate', listener);
|
||||
resolve(dbBlock.result);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
63
src/tests-subnets/env-setup.ts
Normal file
63
src/tests-subnets/env-setup.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { StacksTestnet } from '@stacks/network';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
import { RPCClient } from 'rpc-bitcoin';
|
||||
import { startApiServer } from '../api/init';
|
||||
import { StacksCoreRpcClient } from '../core-rpc/client';
|
||||
import { PgWriteStore } from '../datastore/pg-write-store';
|
||||
import { loadDotEnv } from '../helpers';
|
||||
import { TestEnvContext } from '../test-utils/test-helpers';
|
||||
|
||||
let testEnv: TestEnvContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log('Jest - setup..');
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = 'test';
|
||||
}
|
||||
loadDotEnv();
|
||||
|
||||
process.env.PG_DATABASE = 'postgres';
|
||||
process.env.STACKS_CHAIN_ID = '0x80000000';
|
||||
|
||||
const db = await PgWriteStore.connect({ usageName: 'tests' });
|
||||
const api = await startApiServer({ datastore: db, writeDatastore: db, chainId: ChainID.Testnet });
|
||||
const client = new StacksCoreRpcClient();
|
||||
const stacksNetwork = new StacksTestnet({ url: `http://${client.endpoint}` });
|
||||
|
||||
const { BTC_RPC_PORT, BTC_RPC_HOST, BTC_RPC_PW, BTC_RPC_USER } = process.env;
|
||||
if (!BTC_RPC_PORT || !BTC_RPC_HOST || !BTC_RPC_PW || !BTC_RPC_USER) {
|
||||
throw new Error('Bitcoin JSON-RPC env vars not fully configured.');
|
||||
}
|
||||
const bitcoinRpcClient = new RPCClient({
|
||||
url: BTC_RPC_HOST,
|
||||
port: Number(BTC_RPC_PORT),
|
||||
user: BTC_RPC_USER,
|
||||
pass: BTC_RPC_PW,
|
||||
timeout: 120000,
|
||||
wallet: '',
|
||||
});
|
||||
|
||||
testEnv = {
|
||||
db,
|
||||
api,
|
||||
client,
|
||||
stacksNetwork,
|
||||
bitcoinRpcClient,
|
||||
};
|
||||
Object.assign(global, { testEnv });
|
||||
|
||||
console.log('Jest - setup done');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
process.on('uncaughtException', (error, origin) => {
|
||||
console.error(`____[env-setup] uncaughtException: ${error}, ${origin}`);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error(`____[env-setup] unhandledRejection: ${reason}, ${promise}`);
|
||||
});
|
||||
console.log('Jest - teardown..');
|
||||
await testEnv.api.terminate();
|
||||
await testEnv.db?.close();
|
||||
console.log('Jest - teardown done');
|
||||
});
|
||||
74
src/tests-subnets/global-setup.ts
Normal file
74
src/tests-subnets/global-setup.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { loadDotEnv, timeout } from '../helpers';
|
||||
import { StacksCoreRpcClient } from '../core-rpc/client';
|
||||
import { PgWriteStore } from '../datastore/pg-write-store';
|
||||
import { cycleMigrations } from '../datastore/migrations';
|
||||
import { EventStreamServer, startEventServer } from '../event-stream/event-server';
|
||||
import { ApiServer, startApiServer } from '../api/init';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
import { StacksNetwork, StacksTestnet } from '@stacks/network';
|
||||
|
||||
export interface GlobalTestEnv {
|
||||
db: PgWriteStore;
|
||||
eventServer: EventStreamServer;
|
||||
}
|
||||
|
||||
async function standByForInfoToBeReady(client: StacksCoreRpcClient): Promise<void> {
|
||||
let tries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
tries++;
|
||||
await client.getInfo();
|
||||
return;
|
||||
await timeout(500);
|
||||
} catch (error) {
|
||||
console.log(`Waiting on /v2/info to be ready, retrying after ${error}`);
|
||||
await timeout(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function standByForPox2ToBeReady(client: StacksCoreRpcClient): Promise<void> {
|
||||
let tries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
tries++;
|
||||
const poxInfo = await client.getPox();
|
||||
if (poxInfo.contract_id.includes('pox-2')) {
|
||||
return;
|
||||
}
|
||||
await timeout(500);
|
||||
} catch (error) {
|
||||
console.log(`Waiting on PoX-2 to be ready, retrying after ${error}`);
|
||||
await timeout(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ts-unused-exports:disable-next-line
|
||||
export default async (): Promise<void> => {
|
||||
console.log('Jest - global setup..');
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = 'test';
|
||||
}
|
||||
loadDotEnv();
|
||||
process.env.PG_DATABASE = 'postgres';
|
||||
process.env.STACKS_CHAIN_ID = '0x80000000';
|
||||
|
||||
const db = await PgWriteStore.connect({ usageName: 'tests', withNotifier: false });
|
||||
await cycleMigrations();
|
||||
const eventServer = await startEventServer({ datastore: db, chainId: ChainID.Testnet });
|
||||
|
||||
const subnetClient = new StacksCoreRpcClient();
|
||||
await standByForInfoToBeReady(subnetClient);
|
||||
|
||||
const l1Client = new StacksCoreRpcClient({ port: 20443 });
|
||||
await standByForPox2ToBeReady(l1Client);
|
||||
|
||||
const testEnv: GlobalTestEnv = {
|
||||
db: db,
|
||||
eventServer: eventServer,
|
||||
};
|
||||
Object.assign(global, { globalTestEnv: testEnv });
|
||||
|
||||
console.log('Jest - global setup done');
|
||||
};
|
||||
19
src/tests-subnets/global-teardown.ts
Normal file
19
src/tests-subnets/global-teardown.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { GlobalTestEnv } from './global-setup';
|
||||
|
||||
// ts-unused-exports:disable-next-line
|
||||
export default async (): Promise<void> => {
|
||||
process.on('uncaughtException', (error, origin) => {
|
||||
console.error(`____[global-teardown] uncaughtException: ${error}, ${origin}`);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error(`____[global-teardown] unhandledRejection: ${reason}, ${promise}`);
|
||||
});
|
||||
|
||||
console.log('Jest - global teardown..');
|
||||
const testEnv: GlobalTestEnv = (global as any).globalTestEnv;
|
||||
|
||||
await testEnv.eventServer.closeAsync();
|
||||
await testEnv.db.close();
|
||||
|
||||
console.log('Jest - global teardown done');
|
||||
};
|
||||
15
src/tests-subnets/l1-contracts/nft-trait.clar
Normal file
15
src/tests-subnets/l1-contracts/nft-trait.clar
Normal file
@@ -0,0 +1,15 @@
|
||||
(define-trait nft-trait
|
||||
(
|
||||
;; Last token ID, limited to uint range
|
||||
(get-last-token-id () (response uint uint))
|
||||
|
||||
;; URI for metadata associated with the token
|
||||
(get-token-uri (uint) (response (optional (string-ascii 256)) uint))
|
||||
|
||||
;; Owner of a given token identifier
|
||||
(get-owner (uint) (response (optional principal) uint))
|
||||
|
||||
;; Transfer from the sender to a new principal
|
||||
(transfer (uint principal principal) (response bool uint))
|
||||
)
|
||||
)
|
||||
57
src/tests-subnets/l1-contracts/simple-ft-l1.clar
Normal file
57
src/tests-subnets/l1-contracts/simple-ft-l1.clar
Normal file
@@ -0,0 +1,57 @@
|
||||
(define-constant ERR_NOT_AUTHORIZED (err u1001))
|
||||
|
||||
(impl-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.sip-traits.ft-trait)
|
||||
(impl-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.subnet-traits.mint-from-subnet-trait)
|
||||
|
||||
(define-fungible-token ft-token)
|
||||
|
||||
;; get the token balance of owner
|
||||
(define-read-only (get-balance (owner principal))
|
||||
(begin
|
||||
(ok (ft-get-balance ft-token owner))))
|
||||
|
||||
;; returns the total number of tokens
|
||||
(define-read-only (get-total-supply)
|
||||
(ok (ft-get-supply ft-token)))
|
||||
|
||||
;; returns the token name
|
||||
(define-read-only (get-name)
|
||||
(ok "ft-token"))
|
||||
|
||||
;; the symbol or "ticker" for this token
|
||||
(define-read-only (get-symbol)
|
||||
(ok "EXFT"))
|
||||
|
||||
;; the number of decimals used
|
||||
(define-read-only (get-decimals)
|
||||
(ok u0))
|
||||
|
||||
;; Implement mint-from-subnet trait
|
||||
(define-public (mint-from-subnet (amount uint) (sender principal) (recipient principal))
|
||||
(begin
|
||||
;; Check that the tx-sender is the provided sender
|
||||
(asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED)
|
||||
|
||||
(ft-mint? ft-token amount recipient)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
;; Transfers tokens to a recipient
|
||||
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
|
||||
(begin
|
||||
(try! (ft-transfer? ft-token amount sender recipient))
|
||||
(print memo)
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (get-token-uri)
|
||||
(ok none))
|
||||
|
||||
(define-public (gift-tokens (recipient principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender recipient) ERR_NOT_AUTHORIZED)
|
||||
(ft-mint? ft-token u1 recipient)
|
||||
)
|
||||
)
|
||||
55
src/tests-subnets/l1-contracts/simple-nft-l1.clar
Normal file
55
src/tests-subnets/l1-contracts/simple-nft-l1.clar
Normal file
@@ -0,0 +1,55 @@
|
||||
(define-constant CONTRACT_OWNER tx-sender)
|
||||
(define-constant CONTRACT_ADDRESS (as-contract tx-sender))
|
||||
|
||||
(define-constant ERR_NOT_AUTHORIZED (err u1001))
|
||||
|
||||
(impl-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.nft-trait.nft-trait)
|
||||
(impl-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.subnet-traits.mint-from-subnet-trait)
|
||||
|
||||
(define-data-var lastId uint u0)
|
||||
(define-map CFG_BASE_URI bool (string-ascii 256))
|
||||
|
||||
(define-non-fungible-token nft-token uint)
|
||||
|
||||
(define-read-only (get-last-token-id)
|
||||
(ok (var-get lastId))
|
||||
)
|
||||
|
||||
(define-read-only (get-owner (id uint))
|
||||
(ok (nft-get-owner? nft-token id))
|
||||
)
|
||||
|
||||
(define-read-only (get-token-uri (id uint))
|
||||
(ok (map-get? CFG_BASE_URI true))
|
||||
)
|
||||
|
||||
(define-public (transfer (id uint) (sender principal) (recipient principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED)
|
||||
(nft-transfer? nft-token id sender recipient)
|
||||
)
|
||||
)
|
||||
|
||||
;; test functions
|
||||
(define-public (test-mint (recipient principal))
|
||||
(let
|
||||
((newId (+ (var-get lastId) u1)))
|
||||
(var-set lastId newId)
|
||||
(nft-mint? nft-token newId recipient)
|
||||
)
|
||||
)
|
||||
|
||||
(define-public (mint-from-subnet (id uint) (sender principal) (recipient principal))
|
||||
(begin
|
||||
;; Check that the tx-sender is the provided sender
|
||||
(asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED)
|
||||
|
||||
(nft-mint? nft-token id recipient)
|
||||
)
|
||||
)
|
||||
|
||||
(define-public (gift-nft (recipient principal) (id uint))
|
||||
(begin
|
||||
(nft-mint? nft-token id recipient)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
(define-trait sip-010-trait
|
||||
(
|
||||
;; Transfer from the caller to a new principal
|
||||
(transfer (uint principal principal (optional (buff 34))) (response bool uint))
|
||||
|
||||
;; the human readable name of the token
|
||||
(get-name () (response (string-ascii 32) uint))
|
||||
|
||||
;; the ticker symbol, or empty if none
|
||||
(get-symbol () (response (string-ascii 32) uint))
|
||||
|
||||
;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token
|
||||
(get-decimals () (response uint uint))
|
||||
|
||||
;; the balance of the passed principal
|
||||
(get-balance (principal) (response uint uint))
|
||||
|
||||
;; the current total supply (which does not need to be a constant)
|
||||
(get-total-supply () (response uint uint))
|
||||
|
||||
;; an optional URI that represents metadata of this token
|
||||
(get-token-uri () (response (optional (string-utf8 256)) uint))
|
||||
)
|
||||
)
|
||||
40
src/tests-subnets/l1-contracts/sip-traits.clar
Normal file
40
src/tests-subnets/l1-contracts/sip-traits.clar
Normal file
@@ -0,0 +1,40 @@
|
||||
(define-trait nft-trait
|
||||
(
|
||||
;; Last token ID, limited to uint range
|
||||
(get-last-token-id () (response uint uint))
|
||||
|
||||
;; URI for metadata associated with the token
|
||||
(get-token-uri (uint) (response (optional (string-ascii 256)) uint))
|
||||
|
||||
;; Owner of a given token identifier
|
||||
(get-owner (uint) (response (optional principal) uint))
|
||||
|
||||
;; Transfer from the sender to a new principal
|
||||
(transfer (uint principal principal) (response bool uint))
|
||||
)
|
||||
)
|
||||
|
||||
(define-trait ft-trait
|
||||
(
|
||||
;; Transfer from the caller to a new principal
|
||||
(transfer (uint principal principal (optional (buff 34))) (response bool uint))
|
||||
|
||||
;; the human readable name of the token
|
||||
(get-name () (response (string-ascii 32) uint))
|
||||
|
||||
;; the ticker symbol, or empty if none
|
||||
(get-symbol () (response (string-ascii 32) uint))
|
||||
|
||||
;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token
|
||||
(get-decimals () (response uint uint))
|
||||
|
||||
;; the balance of the passed principal
|
||||
(get-balance (principal) (response uint uint))
|
||||
|
||||
;; the current total supply (which does not need to be a constant)
|
||||
(get-total-supply () (response uint uint))
|
||||
|
||||
;; an optional URI that represents metadata of this token
|
||||
(get-token-uri () (response (optional (string-utf8 256)) uint))
|
||||
)
|
||||
)
|
||||
16
src/tests-subnets/l1-contracts/subnet-traits.clar
Normal file
16
src/tests-subnets/l1-contracts/subnet-traits.clar
Normal file
@@ -0,0 +1,16 @@
|
||||
;; In order to process deposits and withdrawals to a subnet, an asset
|
||||
;; contract must implement this trait.
|
||||
(define-trait mint-from-subnet-trait
|
||||
(
|
||||
;; Process a withdrawal from the subnet for an asset which does not yet
|
||||
;; exist on this network, and thus requires a mint.
|
||||
(mint-from-subnet
|
||||
(
|
||||
uint ;; asset-id (NFT) or amount (FT)
|
||||
principal ;; sender
|
||||
principal ;; recipient
|
||||
)
|
||||
(response bool uint)
|
||||
)
|
||||
)
|
||||
)
|
||||
782
src/tests-subnets/l1-contracts/subnet.clar
Normal file
782
src/tests-subnets/l1-contracts/subnet.clar
Normal file
@@ -0,0 +1,782 @@
|
||||
;; The .subnet contract
|
||||
|
||||
(define-constant CONTRACT_ADDRESS (as-contract tx-sender))
|
||||
|
||||
;; Error codes
|
||||
(define-constant ERR_BLOCK_ALREADY_COMMITTED 1)
|
||||
(define-constant ERR_INVALID_MINER 2)
|
||||
(define-constant ERR_CONTRACT_CALL_FAILED 3)
|
||||
(define-constant ERR_TRANSFER_FAILED 4)
|
||||
(define-constant ERR_DISALLOWED_ASSET 5)
|
||||
(define-constant ERR_ASSET_ALREADY_ALLOWED 6)
|
||||
(define-constant ERR_MERKLE_ROOT_DOES_NOT_MATCH 7)
|
||||
(define-constant ERR_INVALID_MERKLE_ROOT 8)
|
||||
(define-constant ERR_WITHDRAWAL_ALREADY_PROCESSED 9)
|
||||
(define-constant ERR_VALIDATION_FAILED 10)
|
||||
;;; The value supplied for `target-chain-tip` does not match the current chain tip.
|
||||
(define-constant ERR_INVALID_CHAIN_TIP 11)
|
||||
;;; The contract was called before reaching this-chain height reaches 1.
|
||||
(define-constant ERR_CALLED_TOO_EARLY 12)
|
||||
(define-constant ERR_MINT_FAILED 13)
|
||||
(define-constant ERR_ATTEMPT_TO_TRANSFER_ZERO_AMOUNT 14)
|
||||
(define-constant ERR_IN_COMPUTATION 15)
|
||||
;; The contract does not own this NFT to withdraw it.
|
||||
(define-constant ERR_NFT_NOT_OWNED_BY_CONTRACT 16)
|
||||
(define-constant ERR_VALIDATION_LEAF_FAILED 30)
|
||||
|
||||
;; Map from Stacks block height to block commit
|
||||
(define-map block-commits uint (buff 32))
|
||||
;; Map recording withdrawal roots
|
||||
(define-map withdrawal-roots-map (buff 32) bool)
|
||||
;; Map recording processed withdrawal leaves
|
||||
(define-map processed-withdrawal-leaves-map { withdrawal-leaf-hash: (buff 32), withdrawal-root-hash: (buff 32) } bool)
|
||||
|
||||
;; List of miners
|
||||
(define-data-var miner principal 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)
|
||||
|
||||
;; Map of allowed contracts for asset transfers - maps L1 contract principal to L2 contract principal
|
||||
(define-map allowed-contracts principal principal)
|
||||
|
||||
;; Use trait declarations
|
||||
(use-trait nft-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.nft-trait.nft-trait)
|
||||
(use-trait ft-trait 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.sip-010-trait-ft-standard.sip-010-trait)
|
||||
(use-trait mint-from-subnet-trait .subnet-traits.mint-from-subnet-trait)
|
||||
|
||||
;; Update the miner for this contract.
|
||||
(define-public (update-miner (new-miner principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender (var-get miner)) (err ERR_INVALID_MINER))
|
||||
(ok (var-set miner new-miner))
|
||||
)
|
||||
)
|
||||
|
||||
;; Register a new FT contract to be supported by this subnet.
|
||||
(define-public (register-new-ft-contract (ft-contract <ft-trait>) (l2-contract principal))
|
||||
(begin
|
||||
;; Verify that tx-sender is an authorized miner
|
||||
(asserts! (is-miner tx-sender) (err ERR_INVALID_MINER))
|
||||
|
||||
;; Set up the assets that the contract is allowed to transfer
|
||||
(asserts! (map-insert allowed-contracts (contract-of ft-contract) l2-contract)
|
||||
(err ERR_ASSET_ALREADY_ALLOWED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; Register a new NFT contract to be supported by this subnet.
|
||||
(define-public (register-new-nft-contract (nft-contract <nft-trait>) (l2-contract principal))
|
||||
(begin
|
||||
;; Verify that tx-sender is an authorized miner
|
||||
(asserts! (is-miner tx-sender) (err ERR_INVALID_MINER))
|
||||
|
||||
;; Set up the assets that the contract is allowed to transfer
|
||||
(asserts! (map-insert allowed-contracts (contract-of nft-contract) l2-contract)
|
||||
(err ERR_ASSET_ALREADY_ALLOWED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; Helper function: returns a boolean indicating whether the given principal is a miner
|
||||
;; Returns bool
|
||||
(define-private (is-miner (miner-to-check principal))
|
||||
(is-eq miner-to-check (var-get miner))
|
||||
)
|
||||
|
||||
;; Helper function: determines whether the commit-block operation satisfies pre-conditions
|
||||
;; listed in `commit-block`.
|
||||
;; Returns response<bool, int>
|
||||
(define-private (can-commit-block? (commit-block-height uint) (target-chain-tip (buff 32)))
|
||||
(begin
|
||||
;; check no block has been committed at this height
|
||||
(asserts! (is-none (map-get? block-commits commit-block-height)) (err ERR_BLOCK_ALREADY_COMMITTED))
|
||||
|
||||
;; check that `target-chain-tip` matches the burn chain tip
|
||||
(asserts! (is-eq
|
||||
target-chain-tip
|
||||
(unwrap! (get-block-info? id-header-hash (- block-height u1)) (err ERR_CALLED_TOO_EARLY)) )
|
||||
(err ERR_INVALID_CHAIN_TIP))
|
||||
|
||||
;; check that the tx sender is one of the miners
|
||||
(asserts! (is-miner tx-sender) (err ERR_INVALID_MINER))
|
||||
|
||||
;; check that the miner called this contract directly
|
||||
(asserts! (is-miner contract-caller) (err ERR_INVALID_MINER))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; Helper function: modifies the block-commits map with a new commit and prints related info
|
||||
;; Returns response<(buff 32), ?>
|
||||
(define-private (inner-commit-block (block (buff 32)) (commit-block-height uint) (withdrawal-root (buff 32)))
|
||||
(begin
|
||||
(map-set block-commits commit-block-height block)
|
||||
(map-set withdrawal-roots-map withdrawal-root true)
|
||||
(print {
|
||||
event: "block-commit",
|
||||
block-commit: block,
|
||||
withdrawal-root: withdrawal-root,
|
||||
block-height: commit-block-height
|
||||
})
|
||||
(ok block)
|
||||
)
|
||||
)
|
||||
|
||||
;; The subnet miner calls this function to commit a block at a particular height.
|
||||
;; `block` is the hash of the block being submitted.
|
||||
;; `target-chain-tip` is the `id-header-hash` of the burn block (i.e., block on
|
||||
;; this chain) that the miner intends to build off.
|
||||
;;
|
||||
;; Fails if:
|
||||
;; 1) we have already committed at this block height
|
||||
;; 2) `target-chain-tip` is not the burn chain tip (i.e., on this chain)
|
||||
;; 3) the sender is not a miner
|
||||
(define-public (commit-block (block (buff 32)) (target-chain-tip (buff 32)) (withdrawal-root (buff 32)))
|
||||
(let ((commit-block-height block-height))
|
||||
(try! (can-commit-block? commit-block-height target-chain-tip))
|
||||
(inner-commit-block block commit-block-height withdrawal-root)
|
||||
)
|
||||
)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FOR NFT ASSET TRANSFERS
|
||||
|
||||
;; Helper function that transfers the specified NFT from the given sender to the given recipient.
|
||||
;; Returns response<bool, int>
|
||||
(define-private (inner-transfer-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(id uint)
|
||||
(sender principal)
|
||||
(recipient principal)
|
||||
)
|
||||
(let (
|
||||
(call-result (contract-call? nft-contract transfer id sender recipient))
|
||||
(transfer-result (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; Check that the transfer succeeded
|
||||
(asserts! transfer-result (err ERR_TRANSFER_FAILED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-private (inner-mint-nft-asset
|
||||
(nft-mint-contract <mint-from-subnet-trait>)
|
||||
(id uint)
|
||||
(sender principal)
|
||||
(recipient principal)
|
||||
)
|
||||
(let (
|
||||
(call-result (as-contract (contract-call? nft-mint-contract mint-from-subnet id sender recipient)))
|
||||
(mint-result (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; Check that the transfer succeeded
|
||||
(asserts! mint-result (err ERR_MINT_FAILED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-private (inner-transfer-or-mint-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(nft-mint-contract <mint-from-subnet-trait>)
|
||||
(id uint)
|
||||
(recipient principal)
|
||||
)
|
||||
(let (
|
||||
(call-result (contract-call? nft-contract get-owner id))
|
||||
(nft-owner (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
(contract-owns-nft (is-eq nft-owner (some CONTRACT_ADDRESS)))
|
||||
(no-owner (is-eq nft-owner none))
|
||||
)
|
||||
|
||||
(if contract-owns-nft
|
||||
(inner-transfer-nft-asset nft-contract id CONTRACT_ADDRESS recipient)
|
||||
(if no-owner
|
||||
;; Try minting the asset if there is no existing owner of this NFT
|
||||
(inner-mint-nft-asset nft-mint-contract id CONTRACT_ADDRESS recipient)
|
||||
;; In this case, a principal other than this contract owns this NFT, so minting is not possible
|
||||
(err ERR_MINT_FAILED)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
;; A user calls this function to deposit an NFT into the contract.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (deposit-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(id uint)
|
||||
(sender principal)
|
||||
)
|
||||
(let (
|
||||
;; Check that the asset belongs to the allowed-contracts map
|
||||
(subnet-contract-id (unwrap! (map-get? allowed-contracts (contract-of nft-contract)) (err ERR_DISALLOWED_ASSET)))
|
||||
)
|
||||
|
||||
;; Try to transfer the NFT to this contract
|
||||
(asserts! (try! (inner-transfer-nft-asset nft-contract id sender CONTRACT_ADDRESS)) (err ERR_TRANSFER_FAILED))
|
||||
|
||||
;; Emit a print event - the node consumes this
|
||||
(print {
|
||||
event: "deposit-nft",
|
||||
l1-contract-id: (as-contract nft-contract),
|
||||
nft-id: id,
|
||||
sender: sender,
|
||||
subnet-contract-id: subnet-contract-id,
|
||||
})
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
;; Helper function for `withdraw-nft-asset`
|
||||
;; Returns response<bool, int>
|
||||
(define-public (inner-withdraw-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(l2-contract principal)
|
||||
(id uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
(nft-mint-contract (optional <mint-from-subnet-trait>))
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(let ((hashes-are-valid (check-withdrawal-hashes withdrawal-root withdrawal-leaf-hash sibling-hashes)))
|
||||
|
||||
(asserts! (try! hashes-are-valid) (err ERR_VALIDATION_FAILED))
|
||||
|
||||
;; check that the withdrawal request data matches the supplied leaf hash
|
||||
(asserts! (is-eq withdrawal-leaf-hash
|
||||
(leaf-hash-withdraw-nft l2-contract id recipient withdrawal-id height))
|
||||
(err ERR_VALIDATION_LEAF_FAILED))
|
||||
|
||||
(asserts!
|
||||
(try!
|
||||
(match nft-mint-contract
|
||||
mint-contract (as-contract (inner-transfer-or-mint-nft-asset nft-contract mint-contract id recipient))
|
||||
(as-contract (inner-transfer-without-mint-nft-asset nft-contract id recipient))
|
||||
)
|
||||
)
|
||||
(err ERR_TRANSFER_FAILED)
|
||||
)
|
||||
|
||||
(asserts!
|
||||
(finish-withdraw { withdrawal-leaf-hash: withdrawal-leaf-hash, withdrawal-root-hash: withdrawal-root })
|
||||
(err ERR_WITHDRAWAL_ALREADY_PROCESSED)
|
||||
)
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; A user calls this function to withdraw the specified NFT from this contract.
|
||||
;; In order for this withdrawal to go through, the given withdrawal must have been included
|
||||
;; in a withdrawal Merkle tree a subnet miner submitted. The user must provide the leaf
|
||||
;; hash of their withdrawal and the root hash of the specific Merkle tree their withdrawal
|
||||
;; is included in. They must also provide a list of sibling hashes. The withdraw function
|
||||
;; uses the provided hashes to ensure the requested withdrawal is valid.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (withdraw-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(id uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
(nft-mint-contract (optional <mint-from-subnet-trait>))
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(let (
|
||||
;; Check that the asset belongs to the allowed-contracts map
|
||||
(l2-contract (unwrap! (map-get? allowed-contracts (contract-of nft-contract)) (err ERR_DISALLOWED_ASSET)))
|
||||
)
|
||||
(asserts!
|
||||
(try! (inner-withdraw-nft-asset
|
||||
nft-contract
|
||||
l2-contract
|
||||
id
|
||||
recipient
|
||||
withdrawal-id
|
||||
height
|
||||
nft-mint-contract
|
||||
withdrawal-root
|
||||
withdrawal-leaf-hash
|
||||
sibling-hashes
|
||||
))
|
||||
(err ERR_TRANSFER_FAILED)
|
||||
)
|
||||
|
||||
;; Emit a print event
|
||||
(print {
|
||||
event: "withdraw-nft",
|
||||
l1-contract-id: (as-contract nft-contract),
|
||||
nft-id: id,
|
||||
recipient: recipient
|
||||
})
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
;; Like `inner-transfer-or-mint-nft-asset but without allowing or requiring a mint function. In order to withdraw, the user must
|
||||
;; have the appropriate balance.
|
||||
(define-private (inner-transfer-without-mint-nft-asset
|
||||
(nft-contract <nft-trait>)
|
||||
(id uint)
|
||||
(recipient principal)
|
||||
)
|
||||
(let (
|
||||
(call-result (contract-call? nft-contract get-owner id))
|
||||
(nft-owner (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
(contract-owns-nft (is-eq nft-owner (some CONTRACT_ADDRESS)))
|
||||
)
|
||||
|
||||
(asserts! contract-owns-nft (err ERR_NFT_NOT_OWNED_BY_CONTRACT))
|
||||
(inner-transfer-nft-asset nft-contract id CONTRACT_ADDRESS recipient)
|
||||
)
|
||||
)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FOR FUNGIBLE TOKEN ASSET TRANSFERS
|
||||
|
||||
;; Helper function that transfers a specified amount of the fungible token from the given sender to the given recipient.
|
||||
;; Returns response<bool, int>
|
||||
(define-private (inner-transfer-ft-asset
|
||||
(ft-contract <ft-trait>)
|
||||
(amount uint)
|
||||
(sender principal)
|
||||
(recipient principal)
|
||||
(memo (optional (buff 34)))
|
||||
)
|
||||
(let (
|
||||
(call-result (contract-call? ft-contract transfer amount sender recipient memo))
|
||||
(transfer-result (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; FIXME: SIP-010 doesn't require that transfer returns (ok true) on success, so is this check necessary?
|
||||
;; Check that the transfer succeeded
|
||||
(asserts! transfer-result (err ERR_TRANSFER_FAILED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-private (inner-mint-ft-asset
|
||||
(ft-mint-contract <mint-from-subnet-trait>)
|
||||
(amount uint)
|
||||
(sender principal)
|
||||
(recipient principal)
|
||||
)
|
||||
(let (
|
||||
(call-result (as-contract (contract-call? ft-mint-contract mint-from-subnet amount sender recipient)))
|
||||
(mint-result (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; Check that the transfer succeeded
|
||||
(asserts! mint-result (err ERR_MINT_FAILED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-private (inner-transfer-or-mint-ft-asset
|
||||
(ft-contract <ft-trait>)
|
||||
(ft-mint-contract <mint-from-subnet-trait>)
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(memo (optional (buff 34)))
|
||||
)
|
||||
(let (
|
||||
(call-result (contract-call? ft-contract get-balance CONTRACT_ADDRESS))
|
||||
(contract-ft-balance (unwrap! call-result (err ERR_CONTRACT_CALL_FAILED)))
|
||||
(contract-owns-enough (>= contract-ft-balance amount))
|
||||
(amount-to-transfer (if contract-owns-enough amount contract-ft-balance))
|
||||
(amount-to-mint (- amount amount-to-transfer))
|
||||
)
|
||||
|
||||
;; Check that the total balance between the transfer and mint is equal to the original balance
|
||||
(asserts! (is-eq amount (+ amount-to-transfer amount-to-mint)) (err ERR_IN_COMPUTATION))
|
||||
|
||||
(and
|
||||
(> amount-to-transfer u0)
|
||||
(try! (inner-transfer-ft-asset ft-contract amount-to-transfer CONTRACT_ADDRESS recipient memo))
|
||||
)
|
||||
(and
|
||||
(> amount-to-mint u0)
|
||||
(try! (inner-mint-ft-asset ft-mint-contract amount-to-mint CONTRACT_ADDRESS recipient))
|
||||
)
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; A user calls this function to deposit a fungible token into the contract.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (deposit-ft-asset
|
||||
(ft-contract <ft-trait>)
|
||||
(amount uint)
|
||||
(sender principal)
|
||||
(memo (optional (buff 34)))
|
||||
)
|
||||
(let (
|
||||
;; Check that the asset belongs to the allowed-contracts map
|
||||
(subnet-contract-id (unwrap! (map-get? allowed-contracts (contract-of ft-contract)) (err ERR_DISALLOWED_ASSET)))
|
||||
)
|
||||
;; Try to transfer the FT to this contract
|
||||
(asserts! (try! (inner-transfer-ft-asset ft-contract amount sender CONTRACT_ADDRESS memo)) (err ERR_TRANSFER_FAILED))
|
||||
|
||||
(let (
|
||||
(ft-name (unwrap! (contract-call? ft-contract get-name) (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; Emit a print event - the node consumes this
|
||||
(print {
|
||||
event: "deposit-ft",
|
||||
l1-contract-id: (as-contract ft-contract),
|
||||
ft-name: ft-name,
|
||||
ft-amount: amount,
|
||||
sender: sender,
|
||||
subnet-contract-id: subnet-contract-id,
|
||||
})
|
||||
)
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; This function performs validity checks related to the withdrawal and performs the withdrawal as well.
|
||||
;; Returns response<bool, int>
|
||||
(define-private (inner-withdraw-ft-asset
|
||||
(ft-contract <ft-trait>)
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
(memo (optional (buff 34)))
|
||||
(ft-mint-contract (optional <mint-from-subnet-trait>))
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(let ((hashes-are-valid (check-withdrawal-hashes withdrawal-root withdrawal-leaf-hash sibling-hashes)))
|
||||
(asserts! (try! hashes-are-valid) (err ERR_VALIDATION_FAILED))
|
||||
|
||||
;; check that the withdrawal request data matches the supplied leaf hash
|
||||
(asserts! (is-eq withdrawal-leaf-hash
|
||||
(leaf-hash-withdraw-ft (contract-of ft-contract) amount recipient withdrawal-id height))
|
||||
(err ERR_VALIDATION_LEAF_FAILED))
|
||||
|
||||
(asserts!
|
||||
(try!
|
||||
(match ft-mint-contract
|
||||
mint-contract (as-contract (inner-transfer-or-mint-ft-asset ft-contract mint-contract amount recipient memo))
|
||||
(as-contract (inner-transfer-ft-asset ft-contract amount CONTRACT_ADDRESS recipient memo))
|
||||
)
|
||||
)
|
||||
(err ERR_TRANSFER_FAILED)
|
||||
)
|
||||
|
||||
(asserts!
|
||||
(finish-withdraw { withdrawal-leaf-hash: withdrawal-leaf-hash, withdrawal-root-hash: withdrawal-root })
|
||||
(err ERR_WITHDRAWAL_ALREADY_PROCESSED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; A user can call this function to withdraw some amount of a fungible token asset from the
|
||||
;; contract and send it to a recipient.
|
||||
;; In order for this withdrawal to go through, the given withdrawal must have been included
|
||||
;; in a withdrawal Merkle tree a subnet miner submitted. The user must provide the leaf
|
||||
;; hash of their withdrawal and the root hash of the specific Merkle tree their withdrawal
|
||||
;; is included in. They must also provide a list of sibling hashes. The withdraw function
|
||||
;; uses the provided hashes to ensure the requested withdrawal is valid.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (withdraw-ft-asset
|
||||
(ft-contract <ft-trait>)
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
(memo (optional (buff 34)))
|
||||
(ft-mint-contract (optional <mint-from-subnet-trait>))
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(begin
|
||||
;; Check that the withdraw amount is positive
|
||||
(asserts! (> amount u0) (err ERR_ATTEMPT_TO_TRANSFER_ZERO_AMOUNT))
|
||||
|
||||
;; Check that the asset belongs to the allowed-contracts map
|
||||
(unwrap! (map-get? allowed-contracts (contract-of ft-contract)) (err ERR_DISALLOWED_ASSET))
|
||||
|
||||
(asserts!
|
||||
(try! (inner-withdraw-ft-asset
|
||||
ft-contract
|
||||
amount
|
||||
recipient
|
||||
withdrawal-id
|
||||
height
|
||||
memo
|
||||
ft-mint-contract
|
||||
withdrawal-root
|
||||
withdrawal-leaf-hash
|
||||
sibling-hashes))
|
||||
(err ERR_TRANSFER_FAILED)
|
||||
)
|
||||
|
||||
(let (
|
||||
(ft-name (unwrap! (contract-call? ft-contract get-name) (err ERR_CONTRACT_CALL_FAILED)))
|
||||
)
|
||||
;; Emit a print event
|
||||
(print {
|
||||
event: "withdraw-ft",
|
||||
l1-contract-id: (as-contract ft-contract),
|
||||
ft-name: ft-name,
|
||||
ft-amount: amount,
|
||||
recipient: recipient,
|
||||
})
|
||||
)
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FOR STX TRANSFERS
|
||||
|
||||
|
||||
;; Helper function that transfers the given amount from the specified fungible token from the given sender to the given recipient.
|
||||
;; Returns response<bool, int>
|
||||
(define-private (inner-transfer-stx (amount uint) (sender principal) (recipient principal))
|
||||
(let (
|
||||
(call-result (stx-transfer? amount sender recipient))
|
||||
(transfer-result (unwrap! call-result (err ERR_TRANSFER_FAILED)))
|
||||
)
|
||||
;; Check that the transfer succeeded
|
||||
(asserts! transfer-result (err ERR_TRANSFER_FAILED))
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
;; A user calls this function to deposit STX into the contract.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (deposit-stx (amount uint) (sender principal))
|
||||
(begin
|
||||
;; Try to transfer the STX to this contract
|
||||
(asserts! (try! (inner-transfer-stx amount sender CONTRACT_ADDRESS)) (err ERR_TRANSFER_FAILED))
|
||||
|
||||
;; Emit a print event - the node consumes this
|
||||
(print { event: "deposit-stx", sender: sender, amount: amount })
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (leaf-hash-withdraw-stx
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
)
|
||||
(sha512/256 (concat 0x00 (unwrap-panic (to-consensus-buff?
|
||||
{
|
||||
type: "stx",
|
||||
amount: amount,
|
||||
recipient: recipient,
|
||||
withdrawal-id: withdrawal-id,
|
||||
height: height
|
||||
})))
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (leaf-hash-withdraw-nft
|
||||
(asset-contract principal)
|
||||
(nft-id uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
)
|
||||
(sha512/256 (concat 0x00 (unwrap-panic (to-consensus-buff?
|
||||
{
|
||||
type: "nft",
|
||||
nft-id: nft-id,
|
||||
asset-contract: asset-contract,
|
||||
recipient: recipient,
|
||||
withdrawal-id: withdrawal-id,
|
||||
height: height
|
||||
})))
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (leaf-hash-withdraw-ft
|
||||
(asset-contract principal)
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
)
|
||||
(sha512/256 (concat 0x00 (unwrap-panic (to-consensus-buff?
|
||||
{
|
||||
type: "ft",
|
||||
amount: amount,
|
||||
asset-contract: asset-contract,
|
||||
recipient: recipient,
|
||||
withdrawal-id: withdrawal-id,
|
||||
height: height
|
||||
})))
|
||||
)
|
||||
)
|
||||
|
||||
;; A user calls this function to withdraw STX from this contract.
|
||||
;; In order for this withdrawal to go through, the given withdrawal must have been included
|
||||
;; in a withdrawal Merkle tree a subnet miner submitted. The user must provide the leaf
|
||||
;; hash of their withdrawal and the root hash of the specific Merkle tree their withdrawal
|
||||
;; is included in. They must also provide a list of sibling hashes. The withdraw function
|
||||
;; uses the provided hashes to ensure the requested withdrawal is valid.
|
||||
;; The function emits a print with details of this event.
|
||||
;; Returns response<bool, int>
|
||||
(define-public (withdraw-stx
|
||||
(amount uint)
|
||||
(recipient principal)
|
||||
(withdrawal-id uint)
|
||||
(height uint)
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(let ((hashes-are-valid (check-withdrawal-hashes withdrawal-root withdrawal-leaf-hash sibling-hashes)))
|
||||
|
||||
(asserts! (try! hashes-are-valid) (err ERR_VALIDATION_FAILED))
|
||||
;; check that the withdrawal request data matches the supplied leaf hash
|
||||
(asserts! (is-eq withdrawal-leaf-hash
|
||||
(leaf-hash-withdraw-stx amount recipient withdrawal-id height))
|
||||
(err ERR_VALIDATION_LEAF_FAILED))
|
||||
|
||||
(asserts! (try! (as-contract (inner-transfer-stx amount tx-sender recipient))) (err ERR_TRANSFER_FAILED))
|
||||
|
||||
(asserts!
|
||||
(finish-withdraw { withdrawal-leaf-hash: withdrawal-leaf-hash, withdrawal-root-hash: withdrawal-root })
|
||||
(err ERR_WITHDRAWAL_ALREADY_PROCESSED))
|
||||
|
||||
;; Emit a print event
|
||||
(print { event: "withdraw-stx", recipient: recipient, amount: amount })
|
||||
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; GENERAL WITHDRAWAL FUNCTIONS
|
||||
|
||||
;; This function concats the two given hashes in the correct order. It also prepends the buff `0x01`, which is
|
||||
;; a tag denoting a node (versus a leaf).
|
||||
;; Returns a buff
|
||||
(define-private (create-node-hash
|
||||
(curr-hash (buff 32))
|
||||
(sibling-hash (buff 32))
|
||||
(is-sibling-left-side bool)
|
||||
)
|
||||
(let (
|
||||
(concatted-hash (if is-sibling-left-side
|
||||
(concat sibling-hash curr-hash)
|
||||
(concat curr-hash sibling-hash)
|
||||
))
|
||||
)
|
||||
|
||||
(concat 0x01 concatted-hash)
|
||||
)
|
||||
)
|
||||
|
||||
;; This function hashes the curr hash with its sibling hash.
|
||||
;; Returns (buff 32)
|
||||
(define-private (hash-help
|
||||
(sibling {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
})
|
||||
(curr-node-hash (buff 32))
|
||||
)
|
||||
(let (
|
||||
(sibling-hash (get hash sibling))
|
||||
(is-sibling-left-side (get is-left-side sibling))
|
||||
(new-buff (create-node-hash curr-node-hash sibling-hash is-sibling-left-side))
|
||||
)
|
||||
(sha512/256 new-buff)
|
||||
)
|
||||
)
|
||||
|
||||
;; This function checks:
|
||||
;; - That the provided withdrawal root matches a previously submitted one (passed to the function `commit-block`)
|
||||
;; - That the computed withdrawal root matches a previous valid withdrawal root
|
||||
;; - That the given withdrawal leaf hash has not been previously processed
|
||||
;; Returns response<bool, int>
|
||||
(define-private (check-withdrawal-hashes
|
||||
(withdrawal-root (buff 32))
|
||||
(withdrawal-leaf-hash (buff 32))
|
||||
(sibling-hashes (list 50 {
|
||||
hash: (buff 32),
|
||||
is-left-side: bool,
|
||||
}))
|
||||
)
|
||||
(begin
|
||||
;; Check that the user submitted a valid withdrawal root
|
||||
(asserts! (is-some (map-get? withdrawal-roots-map withdrawal-root)) (err ERR_INVALID_MERKLE_ROOT))
|
||||
|
||||
;; Check that this withdrawal leaf has not been processed before
|
||||
(asserts!
|
||||
(is-none
|
||||
(map-get? processed-withdrawal-leaves-map
|
||||
{ withdrawal-leaf-hash: withdrawal-leaf-hash, withdrawal-root-hash: withdrawal-root }))
|
||||
(err ERR_WITHDRAWAL_ALREADY_PROCESSED))
|
||||
|
||||
(let ((calculated-withdrawal-root (fold hash-help sibling-hashes withdrawal-leaf-hash))
|
||||
(roots-match (is-eq calculated-withdrawal-root withdrawal-root)))
|
||||
(if roots-match
|
||||
(ok true)
|
||||
(err ERR_MERKLE_ROOT_DOES_NOT_MATCH))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
;; This function should be called after the asset in question has been transferred.
|
||||
;; It adds the withdrawal leaf hash to a map of processed leaves. This ensures that
|
||||
;; this withdrawal leaf can't be used again to withdraw additional funds.
|
||||
;; Returns bool
|
||||
(define-private (finish-withdraw
|
||||
(withdraw-info {
|
||||
withdrawal-leaf-hash: (buff 32),
|
||||
withdrawal-root-hash: (buff 32)
|
||||
})
|
||||
)
|
||||
(map-insert processed-withdrawal-leaves-map withdraw-info true)
|
||||
)
|
||||
61
src/tests-subnets/l2-contracts/simple-ft-l2.clar
Normal file
61
src/tests-subnets/l2-contracts/simple-ft-l2.clar
Normal file
@@ -0,0 +1,61 @@
|
||||
(define-constant ERR_NOT_AUTHORIZED (err u1001))
|
||||
|
||||
(impl-trait 'ST000000000000000000002AMW42H.subnet.ft-trait)
|
||||
|
||||
(define-fungible-token ft-token)
|
||||
|
||||
;; get the token balance of owner
|
||||
(define-read-only (get-balance (owner principal))
|
||||
(begin
|
||||
(ok (ft-get-balance ft-token owner))))
|
||||
|
||||
;; returns the total number of tokens
|
||||
(define-read-only (get-total-supply)
|
||||
(ok (ft-get-supply ft-token)))
|
||||
|
||||
;; returns the token name
|
||||
(define-read-only (get-name)
|
||||
(ok "ft-token"))
|
||||
|
||||
;; the symbol or "ticker" for this token
|
||||
(define-read-only (get-symbol)
|
||||
(ok "EXFT"))
|
||||
|
||||
;; the number of decimals used
|
||||
(define-read-only (get-decimals)
|
||||
(ok u0))
|
||||
|
||||
;; Transfers tokens to a recipient
|
||||
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
|
||||
(begin
|
||||
(try! (ft-transfer? ft-token amount sender recipient))
|
||||
(print memo)
|
||||
(ok true)
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (get-token-uri)
|
||||
(ok none)
|
||||
)
|
||||
|
||||
(define-read-only (get-token-balance (user principal))
|
||||
(ft-get-balance ft-token user)
|
||||
)
|
||||
|
||||
(impl-trait 'ST000000000000000000002AMW42H.subnet.subnet-asset)
|
||||
|
||||
;; Called for deposit from the burnchain to the subnet
|
||||
(define-public (deposit-from-burnchain (amount uint) (recipient principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender 'ST000000000000000000002AMW42H) ERR_NOT_AUTHORIZED)
|
||||
(ft-mint? ft-token amount recipient)
|
||||
)
|
||||
)
|
||||
|
||||
;; Called for withdrawal from the subnet to the burnchain
|
||||
(define-public (burn-for-withdrawal (amount uint) (owner principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender owner) ERR_NOT_AUTHORIZED)
|
||||
(ft-burn? ft-token amount owner)
|
||||
)
|
||||
)
|
||||
68
src/tests-subnets/l2-contracts/simple-nft-l2.clar
Normal file
68
src/tests-subnets/l2-contracts/simple-nft-l2.clar
Normal file
@@ -0,0 +1,68 @@
|
||||
(define-constant CONTRACT_OWNER tx-sender)
|
||||
(define-constant CONTRACT_ADDRESS (as-contract tx-sender))
|
||||
|
||||
(define-constant ERR_NOT_AUTHORIZED (err u1001))
|
||||
|
||||
(impl-trait 'ST000000000000000000002AMW42H.subnet.nft-trait)
|
||||
|
||||
(define-data-var lastId uint u0)
|
||||
|
||||
(define-non-fungible-token nft-token uint)
|
||||
|
||||
|
||||
;; NFT trait functions
|
||||
(define-read-only (get-last-token-id)
|
||||
(ok (var-get lastId))
|
||||
)
|
||||
|
||||
(define-read-only (get-owner (id uint))
|
||||
(ok (nft-get-owner? nft-token id))
|
||||
)
|
||||
|
||||
(define-read-only (get-token-uri (id uint))
|
||||
(ok (some "unimplemented"))
|
||||
)
|
||||
|
||||
(define-public (transfer (id uint) (sender principal) (recipient principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED)
|
||||
(nft-transfer? nft-token id sender recipient)
|
||||
)
|
||||
)
|
||||
|
||||
;; mint functions
|
||||
(define-public (mint-next (recipient principal))
|
||||
(let
|
||||
((newId (+ (var-get lastId) u1)))
|
||||
(var-set lastId newId)
|
||||
(nft-mint? nft-token newId recipient)
|
||||
)
|
||||
)
|
||||
|
||||
(define-public (gift-nft (recipient principal) (id uint))
|
||||
(begin
|
||||
(nft-mint? nft-token id recipient)
|
||||
)
|
||||
)
|
||||
|
||||
(define-read-only (get-token-owner (id uint))
|
||||
(nft-get-owner? nft-token id)
|
||||
)
|
||||
|
||||
(impl-trait 'ST000000000000000000002AMW42H.subnet.subnet-asset)
|
||||
|
||||
;; Called for deposit from the burnchain to the subnet
|
||||
(define-public (deposit-from-burnchain (id uint) (recipient principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender 'ST000000000000000000002AMW42H) ERR_NOT_AUTHORIZED)
|
||||
(nft-mint? nft-token id recipient)
|
||||
)
|
||||
)
|
||||
|
||||
;; Called for withdrawal from the subnet to the burnchain
|
||||
(define-public (burn-for-withdrawal (id uint) (owner principal))
|
||||
(begin
|
||||
(asserts! (is-eq tx-sender owner) ERR_NOT_AUTHORIZED)
|
||||
(nft-burn? nft-token id owner)
|
||||
)
|
||||
)
|
||||
3
src/tests-subnets/l2-contracts/simple-stx-l2.clar
Normal file
3
src/tests-subnets/l2-contracts/simple-stx-l2.clar
Normal file
@@ -0,0 +1,3 @@
|
||||
(define-public (subnet-withdraw-stx (amount uint) (sender principal))
|
||||
(contract-call? 'ST000000000000000000002AMW42H.subnet stx-withdraw? amount sender)
|
||||
)
|
||||
2
src/tests-subnets/set-env.ts
Normal file
2
src/tests-subnets/set-env.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
process.env['STACKS_CORE_RPC_PORT'] = '30443';
|
||||
process.env['STACKS_CORE_PROXY_PORT'] = '30443';
|
||||
1081
src/tests-subnets/subnet-tests.ts
Normal file
1081
src/tests-subnets/subnet-tests.ts
Normal file
File diff suppressed because it is too large
Load Diff
57
stacks-blockchain/Stacks-subnet.toml
Normal file
57
stacks-blockchain/Stacks-subnet.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
[node]
|
||||
# working_dir = "./devnet"
|
||||
rpc_bind = "0.0.0.0:30443"
|
||||
p2p_bind = "0.0.0.0:30444"
|
||||
miner = true
|
||||
seed = "6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01"
|
||||
mining_key = "cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01"
|
||||
local_peer_seed = "6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01"
|
||||
wait_time_for_microblocks = 50
|
||||
wait_before_first_anchored_block = 0
|
||||
|
||||
[miner]
|
||||
first_attempt_time_ms = 500
|
||||
subsequent_attempt_time_ms = 1000
|
||||
# microblock_attempt_time_ms = 15_000
|
||||
|
||||
[burnchain]
|
||||
chain = "stacks_layer_1"
|
||||
mode = "subnet"
|
||||
first_burn_header_height = 0
|
||||
peer_host = "host.docker.internal"
|
||||
rpc_port = 20443
|
||||
peer_port = 20444
|
||||
contract_identifier = "STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP.subnet"
|
||||
observer_port = 30445
|
||||
poll_time_secs = 1
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01"
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601"
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01"
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "e75dcb66f84287eaf347955e94fa04337298dbd95aa0dbb985771104ef1913db01"
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "ce109fee08860bb16337c76647dcbc02df0c06b455dd69bcf30af74d4eedd19301",
|
||||
|
||||
[[ustx_balance]]
|
||||
address = "ST18MDW2PDTBSCR1ACXYRJP2JX70FWNM6YY2VX4SS"
|
||||
amount = 10000000000000000
|
||||
# secretKey = "08c14a1eada0dd42b667b40f59f7c8dedb12113613448dc04980aea20b268ddb01",
|
||||
26
tests/jest.config.subnets.js
Normal file
26
tests/jest.config.subnets.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
rootDir: `${require('path').dirname(__dirname)}/src`,
|
||||
testMatch: ['<rootDir>/tests-subnets/**/*.ts'],
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/tests-subnets/global-setup.ts',
|
||||
'<rootDir>/tests-subnets/global-teardown.ts',
|
||||
'<rootDir>/tests-subnets/env-setup.ts',
|
||||
'<rootDir>/tests-subnets/test-helpers.ts',
|
||||
'<rootDir>/tests-subnets/set-env.ts',
|
||||
],
|
||||
collectCoverageFrom: ['<rootDir>/**/*.ts'],
|
||||
coveragePathIgnorePatterns: ['<rootDir>/tests*'],
|
||||
coverageDirectory: '<rootDir>/../coverage',
|
||||
globalSetup: '<rootDir>/tests-subnets/global-setup.ts',
|
||||
globalTeardown: '<rootDir>/tests-subnets/global-teardown.ts',
|
||||
setupFilesAfterEnv: ['<rootDir>/tests-subnets/env-setup.ts'],
|
||||
setupFiles: ['<rootDir>/tests-subnets/set-env.ts'],
|
||||
testTimeout: 60_000,
|
||||
verbose: true,
|
||||
bail: true,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user