feat: dev-stack & bootstrap (#12)

* feat: dev-stack & bootstrap

Signed-off-by: bestmike007 <i@bestmike007.com>

* chore: contract codegen

Signed-off-by: bestmike007 <i@bestmike007.com>

* chore: add depends_on

Signed-off-by: bestmike007 <i@bestmike007.com>

---------

Signed-off-by: bestmike007 <i@bestmike007.com>
This commit is contained in:
Yuanhai He
2023-08-25 14:55:53 +08:00
committed by GitHub
parent a6e9ea8633
commit 4d80ef4f17
33 changed files with 2876 additions and 3 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.cache
node_modules
pnpm-lock.yaml
pnpm-lock.yaml
/bootstrap/config.json

View File

@@ -31,5 +31,4 @@ epoch = 2.1
path = 'contracts/indexer.clar'
clarity_version = 2
epoch = 2.1
depends_on = ["clarity-bitcoin"]

10
bootstrap/constants.ts Normal file
View File

@@ -0,0 +1,10 @@
import config from './config.json';
export const DEPLOYER_ACCOUNT_ADDRESS = () => config.DEPLOYER_ACCOUNT_ADDRESS;
export const DEPLOYER_ACCOUNT_SECRETKEY = () =>
config.DEPLOYER_ACCOUNT_SECRETKEY;
export const STACKS_API_URL = () =>
process.env.STACKS_API_URL || config.STACKS_API_URL;
export const STACKS_PUPPET_URL = () =>
process.env.STACKS_PUPPET_URL || config.STACKS_PUPPET_URL;
export const USER_ACCOUNTS = () => config.USER_ACCOUNTS;

View File

@@ -0,0 +1,7 @@
import type contracts from './contracts.json';
export const Contracts: Array<keyof typeof contracts> = [
'utils',
'clarity-bitcoin',
'indexer',
];

View File

@@ -0,0 +1,17 @@
{
"clarity-bitcoin": {
"path": "contracts/clarity-bitcoin.clar",
"clarity_version": 2,
"epoch": 2.1
},
"utils": {
"path": "contracts/utils.clar",
"clarity_version": 2,
"epoch": 2.1
},
"indexer": {
"path": "contracts/indexer.clar",
"clarity_version": 2,
"epoch": 2.1
}
}

View File

@@ -0,0 +1,12 @@
import toml from '@iarna/toml';
import fs from 'fs';
import path from 'path';
const clarinetConfig = toml.parse(
fs.readFileSync(path.resolve(__dirname, '../..', 'Clarinet.toml'), 'utf8'),
);
fs.writeFileSync(
path.resolve(__dirname, './contracts.json'),
JSON.stringify(clarinetConfig.contracts, null, 2) + '\n',
);

View File

@@ -0,0 +1,14 @@
import { generateContracts } from 'clarity-codegen/lib/generate';
import * as path from 'path';
import { DEPLOYER_ACCOUNT_ADDRESS, STACKS_API_URL } from '../constants';
import { Contracts } from './contractNames';
(async function main() {
await generateContracts(
STACKS_API_URL(),
DEPLOYER_ACCOUNT_ADDRESS(),
Contracts,
path.resolve(__dirname, './generated/'),
'Brc20Indexer',
);
})().catch(console.error);

View File

@@ -0,0 +1,399 @@
import {
defineContract,
numberT,
bufferT,
responseSimpleT,
booleanT,
optionalT,
tupleT,
listT
} from "clarity-codegen"
export const clarityBitcoin = defineContract({
"clarity-bitcoin": {
'mock-add-burnchain-block-header-hash': {
input: [
{ name: 'burn-height', type: numberT },
{ name: 'hash', type: bufferT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'get-bc-h-hash': {
input: [ { name: 'bh', type: numberT } ],
output: optionalT(bufferT, ),
mode: 'readonly'
},
'get-reversed-segwit-txid': {
input: [ { name: 'tx', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'get-reversed-txid': {
input: [ { name: 'tx', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'get-segwit-txid': {
input: [ { name: 'tx', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'get-txid': {
input: [ { name: 'tx', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'inner-merkle-proof-verify': {
input: [
{ name: 'ctr', type: numberT },
{
name: 'state',
type: tupleT({
'cur-hash': bufferT,
path: numberT,
'proof-hashes': listT(bufferT, ),
'root-hash': bufferT,
'tree-depth': numberT,
verified: booleanT
}, )
}
],
output: tupleT({
'cur-hash': bufferT,
path: numberT,
'proof-hashes': listT(bufferT, ),
'root-hash': bufferT,
'tree-depth': numberT,
verified: booleanT
}, ),
mode: 'readonly'
},
'inner-reverse': {
input: [
{ name: 'target-index', type: numberT },
{ name: 'hash-input', type: bufferT }
],
output: bufferT,
mode: 'readonly'
},
'is-bit-set': {
input: [ { name: 'val', type: numberT }, { name: 'bit', type: numberT } ],
output: booleanT,
mode: 'readonly'
},
'parse-block-header': {
input: [ { name: 'headerbuff', type: bufferT } ],
output: responseSimpleT(tupleT({
'merkle-root': bufferT,
nbits: numberT,
nonce: numberT,
parent: bufferT,
timestamp: numberT,
version: numberT
}, ), ),
mode: 'readonly'
},
'parse-tx': {
input: [ { name: 'tx', type: bufferT } ],
output: responseSimpleT(tupleT({
ins: listT(tupleT({
outpoint: tupleT({ hash: bufferT, index: numberT }, ),
scriptSig: bufferT,
sequence: numberT
}, ), ),
locktime: numberT,
outs: listT(tupleT({ scriptPubKey: bufferT, value: numberT }, ), ),
version: numberT
}, ), ),
mode: 'readonly'
},
'parse-wtx': {
input: [ { name: 'tx', type: bufferT } ],
output: responseSimpleT(tupleT({
ins: listT(tupleT({
outpoint: tupleT({ hash: bufferT, index: numberT }, ),
scriptSig: bufferT,
sequence: numberT
}, ), ),
locktime: numberT,
outs: listT(tupleT({ scriptPubKey: bufferT, value: numberT }, ), ),
'segwit-marker': numberT,
'segwit-version': numberT,
version: numberT,
witnesses: listT(listT(bufferT, ), )
}, ), ),
mode: 'readonly'
},
'read-hashslice': {
input: [
{
name: 'old-ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
hashslice: bufferT
}, ), ),
mode: 'readonly'
},
'read-next-element': {
input: [
{ name: 'ignored', type: booleanT },
{
name: 'state-res',
type: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
elements: listT(bufferT, )
}, ), )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
elements: listT(bufferT, )
}, ), ),
mode: 'readonly'
},
'read-next-txin': {
input: [
{ name: 'ignored', type: booleanT },
{
name: 'state-res',
type: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
remaining: numberT,
txins: listT(tupleT({
outpoint: tupleT({ hash: bufferT, index: numberT }, ),
scriptSig: bufferT,
sequence: numberT
}, ), )
}, ), )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
remaining: numberT,
txins: listT(tupleT({
outpoint: tupleT({ hash: bufferT, index: numberT }, ),
scriptSig: bufferT,
sequence: numberT
}, ), )
}, ), ),
mode: 'readonly'
},
'read-next-txout': {
input: [
{ name: 'ignored', type: booleanT },
{
name: 'state-res',
type: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
txouts: listT(tupleT({ scriptPubKey: bufferT, value: numberT }, ), )
}, ), )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
txouts: listT(tupleT({ scriptPubKey: bufferT, value: numberT }, ), )
}, ), ),
mode: 'readonly'
},
'read-next-witness': {
input: [
{ name: 'ignored', type: booleanT },
{
name: 'state-res',
type: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
witnesses: listT(listT(bufferT, ), )
}, ), )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
witnesses: listT(listT(bufferT, ), )
}, ), ),
mode: 'readonly'
},
'read-txins': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
remaining: numberT,
txins: listT(tupleT({
outpoint: tupleT({ hash: bufferT, index: numberT }, ),
scriptSig: bufferT,
sequence: numberT
}, ), )
}, ), ),
mode: 'readonly'
},
'read-txouts': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
txouts: listT(tupleT({ scriptPubKey: bufferT, value: numberT }, ), )
}, ), ),
mode: 'readonly'
},
'read-uint16': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({ ctx: tupleT({ index: numberT, txbuff: bufferT }, ), uint16: numberT }, ), ),
mode: 'readonly'
},
'read-uint32': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({ ctx: tupleT({ index: numberT, txbuff: bufferT }, ), uint32: numberT }, ), ),
mode: 'readonly'
},
'read-uint64': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({ ctx: tupleT({ index: numberT, txbuff: bufferT }, ), uint64: numberT }, ), ),
mode: 'readonly'
},
'read-uint8': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({ ctx: tupleT({ index: numberT, txbuff: bufferT }, ), uint8: numberT }, ), ),
mode: 'readonly'
},
'read-varint': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({ ctx: tupleT({ index: numberT, txbuff: bufferT }, ), varint: numberT }, ), ),
mode: 'readonly'
},
'read-varslice': {
input: [
{
name: 'old-ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
}
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
varslice: bufferT
}, ), ),
mode: 'readonly'
},
'read-witnesses': {
input: [
{
name: 'ctx',
type: tupleT({ index: numberT, txbuff: bufferT }, )
},
{ name: 'num-txins', type: numberT }
],
output: responseSimpleT(tupleT({
ctx: tupleT({ index: numberT, txbuff: bufferT }, ),
witnesses: listT(listT(bufferT, ), )
}, ), ),
mode: 'readonly'
},
'reverse-buff32': {
input: [ { name: 'input', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'verify-block-header': {
input: [
{ name: 'headerbuff', type: bufferT },
{ name: 'expected-block-height', type: numberT }
],
output: booleanT,
mode: 'readonly'
},
'verify-merkle-proof': {
input: [
{ name: 'reversed-txid', type: bufferT },
{ name: 'merkle-root', type: bufferT },
{
name: 'proof',
type: tupleT({
hashes: listT(bufferT, ),
'tree-depth': numberT,
'tx-index': numberT
}, )
}
],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'was-segwit-tx-mined?': {
input: [
{
name: 'block',
type: tupleT({ header: bufferT, height: numberT }, )
},
{ name: 'tx', type: bufferT },
{
name: 'proof',
type: tupleT({
hashes: listT(bufferT, ),
'tree-depth': numberT,
'tx-index': numberT
}, )
}
],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'was-tx-mined?': {
input: [
{
name: 'block',
type: tupleT({ header: bufferT, height: numberT }, )
},
{ name: 'tx', type: bufferT },
{
name: 'proof',
type: tupleT({
hashes: listT(bufferT, ),
'tree-depth': numberT,
'tx-index': numberT
}, )
}
],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'mock-burnchain-header-hashes': { input: numberT, output: optionalT(bufferT, ), mode: 'mapEntry' }
}
} as const)

View File

@@ -0,0 +1,193 @@
import {
defineContract,
bufferT,
principalT,
responseSimpleT,
numberT,
booleanT,
listT,
tupleT,
stringT,
optionalT,
noneT
} from "clarity-codegen"
export const indexer = defineContract({
"indexer": {
'add-validator': {
input: [
{ name: 'validator-pubkey', type: bufferT },
{ name: 'validator', type: principalT }
],
output: responseSimpleT(numberT, ),
mode: 'public'
},
'approve-relayer': {
input: [
{ name: 'relayer', type: principalT },
{ name: 'approved', type: booleanT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'index-tx-many': {
input: [
{
name: 'tx-many',
type: listT(tupleT({
block: tupleT({ header: bufferT, height: numberT }, ),
proof: tupleT({
hashes: listT(bufferT, ),
'tree-depth': numberT,
'tx-index': numberT
}, ),
'signature-packs': listT(tupleT({ signature: bufferT, signer: principalT, 'tx-hash': bufferT }, ), ),
tx: tupleT({
amt: numberT,
'bitcoin-tx': bufferT,
from: bufferT,
'from-bal': numberT,
output: numberT,
tick: stringT,
to: bufferT,
'to-bal': numberT
}, )
}, ), )
}
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'remove-validator': {
input: [ { name: 'validator', type: principalT } ],
output: responseSimpleT(numberT, ),
mode: 'public'
},
'set-contract-owner': {
input: [ { name: 'owner', type: principalT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-paused': {
input: [ { name: 'paused', type: booleanT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-required-validators': {
input: [ { name: 'new-required-validators', type: numberT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-user-balance': {
input: [
{ name: 'user', type: bufferT },
{ name: 'tick', type: stringT },
{ name: 'amt', type: numberT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'get-bitcoin-tx-indexed-or-fail': {
input: [
{ name: 'bitcoin-tx', type: bufferT },
{ name: 'output', type: numberT }
],
output: responseSimpleT(tupleT({ amt: numberT, from: bufferT, tick: stringT, to: bufferT }, ), ),
mode: 'readonly'
},
'get-contract-owner': { input: [], output: principalT, mode: 'readonly' },
'get-paused': { input: [], output: booleanT, mode: 'readonly' },
'get-required-validators': { input: [], output: numberT, mode: 'readonly' },
'get-user-balance-or-default': {
input: [
{ name: 'user', type: bufferT },
{ name: 'tick', type: stringT }
],
output: numberT,
mode: 'readonly'
},
'get-validator-or-fail': {
input: [ { name: 'validator', type: principalT } ],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'hash-tx': {
input: [
{
name: 'tx',
type: tupleT({
amt: numberT,
'bitcoin-tx': bufferT,
from: bufferT,
'from-bal': numberT,
output: numberT,
tick: stringT,
to: bufferT,
'to-bal': numberT
}, )
}
],
output: bufferT,
mode: 'readonly'
},
'validate-tx': {
input: [
{ name: 'tx-hash', type: bufferT },
{
name: 'signature-pack',
type: tupleT({ signature: bufferT, signer: principalT, 'tx-hash': bufferT }, )
}
],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'verify-mined': {
input: [
{ name: 'tx', type: bufferT },
{
name: 'block',
type: tupleT({ header: bufferT, height: numberT }, )
},
{
name: 'proof',
type: tupleT({
hashes: listT(bufferT, ),
'tree-depth': numberT,
'tx-index': numberT
}, )
}
],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'approved-relayers': {
input: principalT,
output: optionalT(booleanT, ),
mode: 'mapEntry'
},
'bitcoin-tx-indexed': {
input: tupleT({ output: numberT, 'tx-hash': bufferT }, ),
output: optionalT(tupleT({ amt: numberT, from: bufferT, tick: stringT, to: bufferT }, ), ),
mode: 'mapEntry'
},
'tx-validated-by': {
input: tupleT({ 'tx-hash': bufferT, validator: principalT }, ),
output: optionalT(booleanT, ),
mode: 'mapEntry'
},
'user-balance': {
input: tupleT({ tick: stringT, user: bufferT }, ),
output: optionalT(numberT, ),
mode: 'mapEntry'
},
validators: { input: principalT, output: optionalT(bufferT, ), mode: 'mapEntry' },
'contract-owner': { input: noneT, output: principalT, mode: 'variable' },
'is-paused': { input: noneT, output: booleanT, mode: 'variable' },
'required-validators': { input: noneT, output: numberT, mode: 'variable' },
'tx-hash-to-iter': { input: noneT, output: bufferT, mode: 'variable' },
'validator-count': { input: noneT, output: numberT, mode: 'variable' }
}
} as const)

View File

@@ -0,0 +1,60 @@
import {
defineContract,
bufferT,
numberT,
booleanT,
stringAsciiT
} from "clarity-codegen"
export const utils = defineContract({
"utils": {
'byte-to-uint': {
input: [ { name: 'byte', type: bufferT } ],
output: numberT,
mode: 'readonly'
},
'serialize-bool': {
input: [ { name: 'value', type: booleanT } ],
output: bufferT,
mode: 'readonly'
},
'serialize-buff': {
input: [ { name: 'value', type: bufferT } ],
output: bufferT,
mode: 'readonly'
},
'serialize-string': {
input: [ { name: 'value', type: stringAsciiT } ],
output: bufferT,
mode: 'readonly'
},
'serialize-uint': {
input: [ { name: 'value', type: numberT } ],
output: bufferT,
mode: 'readonly'
},
'string-ascii-to-buff': {
input: [ { name: 'str', type: stringAsciiT } ],
output: bufferT,
mode: 'readonly'
},
'uint-to-byte': {
input: [ { name: 'n', type: numberT } ],
output: bufferT,
mode: 'readonly'
},
'uint128-to-buff-be': {
input: [ { name: 'n', type: numberT } ],
output: bufferT,
mode: 'readonly'
},
'uint32-to-buff-be': {
input: [ { name: 'n', type: numberT } ],
output: bufferT,
mode: 'readonly'
}
}
} as const)

View File

@@ -0,0 +1,12 @@
import { defineContract } from "clarity-codegen";
import { utils } from "./contract_utils"
import { clarityBitcoin } from "./contract_clarity-bitcoin"
import { indexer } from "./contract_indexer"
export const Brc20IndexerContracts = defineContract({
...utils,
...clarityBitcoin,
...indexer
});

View File

@@ -0,0 +1,13 @@
import { DEPLOYER_ACCOUNT_ADDRESS } from '../constants';
import { Brc20IndexerContracts } from './generated/contracts_Brc20Indexer';
export type Contracts = typeof Brc20IndexerContracts;
export type ContractName = keyof Contracts;
export function contractName<T extends ContractName>(name: T): T {
return name;
}
export function principal<T extends ContractName>(contract: T) {
return `${DEPLOYER_ACCOUNT_ADDRESS()}.${String(contract)}`;
}

27
bootstrap/contracts/operation.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
import { ClarityValue } from '@stacks/transactions';
export type Operation =
| Operation.DeployContract
| Operation.PublicCall
| Operation.TransferSTX;
export namespace Operation {
export type TransferSTX = {
type: 'transfer';
amount: number;
address: string;
};
export type DeployContract = {
type: 'deploy';
name: string;
path: string;
};
export type PublicCall = {
type: 'publicCall';
contract: string;
function: string;
args: ClarityValue[];
};
}

View File

@@ -0,0 +1,35 @@
import {
OpenCallFunctionDescriptor,
ParameterObjOfDescriptor,
} from 'clarity-codegen';
import { Brc20IndexerContracts } from './generated/contracts_Brc20Indexer';
import { Operation } from './operation';
export type Contracts = typeof Brc20IndexerContracts;
export type ContractName = keyof Contracts;
export const callPublic = <
T extends ContractName,
F extends keyof Contracts[T],
>(
contractOrType: T,
functionName: F,
args: Contracts[T][F] extends OpenCallFunctionDescriptor
? ParameterObjOfDescriptor<Contracts[T][F]>
: never,
): Operation.PublicCall => {
const descriptor = Brc20IndexerContracts[contractOrType][
functionName
] as any as OpenCallFunctionDescriptor;
return {
type: 'publicCall',
contract: contractOrType as string,
function: functionName as string,
args: descriptor.input.map(a => a.type.encode(args[a.name])),
};
};
export const transferStxTo = (
address: string,
amount: number,
): Operation.TransferSTX => ({ amount, address, type: 'transfer' });

31
bootstrap/deploy.ts Normal file
View File

@@ -0,0 +1,31 @@
import path from 'path';
import {
DEPLOYER_ACCOUNT_ADDRESS,
DEPLOYER_ACCOUNT_SECRETKEY,
STACKS_API_URL,
} from './constants';
import { Contracts } from './contracts/contractNames';
import { deployContracts } from './setup/deployContracts';
import { sleep } from './utils';
import { getAccountInfo, processOperations } from './utils/processOperations';
(async () => {
while (true) {
try {
await getAccountInfo(DEPLOYER_ACCOUNT_ADDRESS());
break;
} catch (e) {
console.log(
`waiting for connecting stacks-node-api: ${STACKS_API_URL()}`,
);
await sleep(500);
}
}
console.log(`starting deploy: ${STACKS_API_URL()}`);
await processOperations(
DEPLOYER_ACCOUNT_ADDRESS(),
DEPLOYER_ACCOUNT_SECRETKEY(),
10 * 1e6,
)(deployContracts(Contracts, path.resolve(__dirname, '..')));
})();

28
bootstrap/faucet.ts Normal file
View File

@@ -0,0 +1,28 @@
import {
DEPLOYER_ACCOUNT_ADDRESS,
DEPLOYER_ACCOUNT_SECRETKEY,
} from './constants';
import { transferStxTo } from './contracts/operationFactory';
import { processOperations } from './utils/processOperations';
const processAsDeployer = processOperations(
DEPLOYER_ACCOUNT_ADDRESS(),
DEPLOYER_ACCOUNT_SECRETKEY(),
1e6,
);
async function faucet() {
const recipient = process.argv[2];
if (recipient == null) {
console.log(`Usage: yarn faucet <address>`);
return;
}
if (!recipient.startsWith('ST') && !recipient.startsWith('SP')) {
console.log(`Invalid stacks address: ${recipient}`);
return;
}
await processAsDeployer([transferStxTo(recipient, 100e6)]);
}
faucet().catch(console.error);

34
bootstrap/setup.ts Normal file
View File

@@ -0,0 +1,34 @@
import got from 'got-cjs';
import { uniq } from 'lodash';
import {
DEPLOYER_ACCOUNT_ADDRESS,
DEPLOYER_ACCOUNT_SECRETKEY,
USER_ACCOUNTS,
} from './constants';
import { transferStxTo } from './contracts/operationFactory';
import { processOperations } from './utils/processOperations';
const processAsDeployer = processOperations(
DEPLOYER_ACCOUNT_ADDRESS(),
DEPLOYER_ACCOUNT_SECRETKEY(),
1e6,
);
async function setup() {
const list: { Address: string }[] = await got
.get(
'https://still-wave-a807-production.reily.workers.dev/v1/table/474f84ab0c8444ef84feae17dee513e8',
)
.json();
const addresses = list.flatMap(({ Address }) => Address.split(','));
await processAsDeployer([
...uniq([...USER_ACCOUNTS().map(u => u.address), ...addresses]).flatMap(
address => [transferStxTo(address, 100e6)],
),
]);
}
setup()
.catch(console.error)
.then(() => process.exit());

View File

@@ -0,0 +1,60 @@
import toml from '@iarna/toml';
import fs from 'fs';
import { uniq } from 'lodash';
import path from 'path';
import { Operation } from '../contracts/operation';
type DeployContractTarget = {
contractName: string;
contractPath: string;
};
type Contracts = {
[key: string]: {
path: string;
depends_on: string[];
};
};
const mapContractsToDeployTarget = (
contractNames: string[],
{ clarinetPath }: { clarinetPath: string },
): DeployContractTarget[] => {
const clarinetConfig = toml.parse(
fs.readFileSync(path.resolve(clarinetPath, 'Clarinet.toml'), 'utf8'),
);
const contracts = clarinetConfig.contracts as Contracts;
function findDeps(name: string): string[] {
if (!contracts[name]) {
throw new Error(`Could not find contract ${name}`);
}
const contract = contracts[name].depends_on ?? [];
return [...contract.flatMap(findDeps), name];
}
const sortedContractNames = uniq(contractNames.flatMap(findDeps));
return sortedContractNames.map(contractName => {
return {
contractName,
contractPath: path.resolve(clarinetPath, contracts[contractName].path),
};
});
};
export function deployContracts(
contracts: string[],
clarinetPath: string,
): Operation.DeployContract[] {
const result = mapContractsToDeployTarget(contracts, {
clarinetPath,
});
console.log(
`Found ${result.length} deploy targets: ${result
.map(r => r.contractName)
.join(', ')}`,
);
return result.map(r => ({
type: 'deploy',
path: r.contractPath,
name: r.contractName,
}));
}

View File

@@ -0,0 +1,3 @@
export function fromUint8Array(arr: Uint8Array): Buffer {
return Buffer.from(arr, arr.byteOffset, arr.byteLength);
}

33
bootstrap/utils/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import { CoreNodeInfoResponse } from '@stacks/stacks-blockchain-api-types';
import { STACKS_API_URL } from '../constants';
export function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x);
}
export function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
export async function getCurrentInfo(): Promise<CoreNodeInfoResponse> {
return fetch(`${STACKS_API_URL()}/v2/info`).then(res => res.json());
}
export function isNotNull<T>(input: T | undefined | null): input is T {
return input != null;
}
export function sleep(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
export async function repeatForever(fn: () => Promise<void>, interval: number) {
// noinspection InfiniteLoopJS
while (true) {
await fn().catch(e => console.error(e));
await sleep(interval);
}
}
export function random<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}

View File

@@ -0,0 +1,289 @@
import { StacksMocknet } from '@stacks/network';
import {
AccountDataResponse,
AddressTransactionsListResponse,
Transaction,
} from '@stacks/stacks-blockchain-api-types';
import {
AnchorMode,
broadcastTransaction,
estimateContractFunctionCall,
makeContractCall,
makeContractDeploy,
makeSTXTokenTransfer,
PostConditionMode,
} from '@stacks/transactions';
import fs from 'fs';
import { assertNever, sleep } from '.';
import {
DEPLOYER_ACCOUNT_ADDRESS,
STACKS_API_URL,
STACKS_PUPPET_URL,
} from '../constants';
import { Operation } from '../contracts/operation';
function jsonReplacer(this: any, key: string) {
const v = this[key];
if (typeof v === 'bigint') return String(v);
return v;
}
export const processOperations =
(address: string, senderKey: string, fee: number = 2 * 1e6) =>
async (operations: Operation[]) => {
const start = Date.now();
const ts = () => `${start}+${Date.now() - start}`;
console.log(`Submitting ${operations.length} operations`);
let startingNonce = (await getAccountInfo(address)).nonce;
console.log(`[${ts()}] starting nonce: ${startingNonce}`);
if (operations.length === 0) return startingNonce;
let serverNonce = startingNonce;
let nonce = serverNonce;
const puppetUrl = STACKS_PUPPET_URL() ?? '';
operations = operations.slice();
const nonceToOperation = new Map<number, Operation>();
let operation: undefined | Operation;
while ((operation = operations.shift())) {
while (nonce > serverNonce + 25) {
if (puppetUrl.length > 0) {
await fetch(`${puppetUrl}/kick`, { method: 'POST' });
await sleep(30);
} else {
await sleep(3 * 1000);
}
serverNonce = (await getAccountInfo(address)).nonce;
}
console.log(`[${ts()}] processing #${nonce - startingNonce}`);
try {
nonceToOperation.set(nonce, operation);
switch (operation.type) {
case 'publicCall':
await publicCall(operation, { senderKey, nonce, fee });
break;
case 'deploy':
await deployContract(operation, { senderKey, nonce, fee });
break;
case 'transfer':
await transferSTX(operation, { senderKey, nonce, fee });
break;
default:
assertNever(operation);
}
nonce++;
} catch (e) {
if ((e as Error).message.includes('ContractAlreadyExists')) {
continue;
}
console.log(`[${ts()}] operation failed:`, operation, e);
}
}
while (nonce !== serverNonce) {
if (puppetUrl.length > 0) {
await fetch(`${puppetUrl}/kick`, { method: 'POST' });
await sleep(100);
} else {
await sleep(3 * 1000);
}
serverNonce = (await getAccountInfo(address)).nonce;
}
if (nonce > startingNonce) {
const txs = await getTransaction(address, startingNonce);
const errTxs = txs.filter(tx => tx.tx_status !== 'success');
if (errTxs.length) {
throw new Error(
`[${ts()}] ${errTxs.length} transactions failed:\n\t${errTxs
.map(
a =>
`tx: ${a.tx_id}\noperation: ${JSON.stringify(
nonceToOperation.get(a.nonce) ?? 'N/A',
jsonReplacer,
)}, result: ${JSON.stringify(a.tx_result)}`,
)
.join('\n\t')}`,
);
}
}
console.log(
`Finished ${nonce - startingNonce} transactions in ${
Date.now() - start
}ms`,
);
return nonce;
};
const network = new StacksMocknet({ url: STACKS_API_URL() });
function hashCode(str: string) {
let hash = 0,
i = 0,
len = str.length;
while (i < len) {
hash = ((hash << 5) - hash + str.charCodeAt(i++)) << 0;
}
return hash + 2147483647 + 1;
// return hash;
}
// Replace all ERR- for debug purposes
const codeMap: {
[code: string]: {
code: string;
comment: string;
};
} = {};
function processError(name: string, input: string) {
const lines = input.split('\n');
const result = lines
.map((line, index) => {
if (line.includes('define-constant')) {
return line;
}
if (!line.includes('ERR-')) {
return line;
}
const location = `${name}.clar:${index + 1}`;
const code = hashCode(location).toString();
const searchValue = /ERR-[A-Z-]+/g;
codeMap[code] = {
code: line.match(searchValue)?.join(',') ?? 'UNKNOWN_CODE',
comment: location,
};
return line.replaceAll(searchValue, `(err u${code})`); //?
})
.filter(x => Boolean(x) && !x.startsWith(';;'))
.join('\n');
fs.writeFileSync('./codeMap.json', JSON.stringify(codeMap, null, 2) + '\n', {
encoding: 'utf-8',
});
return result;
}
async function deployContract(
operation: Operation.DeployContract,
options: OperationOptions,
) {
const txOptions = {
contractName: operation.name,
codeBody: processError(
operation.name,
fs.readFileSync(operation.path, 'utf8'),
),
nonce: options.nonce,
network,
anchorMode: AnchorMode.Any,
postConditionMode: PostConditionMode.Allow,
senderKey: options.senderKey,
fee: options.fee,
};
const fee = await estimateContractFunctionCall(
await makeContractDeploy(txOptions),
network,
).catch(() => options.fee);
const result = await broadcastTransaction(
await makeContractDeploy({
...txOptions,
fee,
}),
network,
);
if (result.error) {
throw new Error(result.reason!);
}
}
async function transferSTX(
operation: Operation.TransferSTX,
options: OperationOptions,
) {
const txOptions = {
network,
nonce: options.nonce,
fee: options.fee,
anchorMode: AnchorMode.Any,
postConditionMode: PostConditionMode.Allow,
senderKey: options.senderKey,
amount: operation.amount,
recipient: operation.address,
};
const fee = await estimateContractFunctionCall(
await makeSTXTokenTransfer(txOptions),
network,
).catch(() => options.fee);
const result = await broadcastTransaction(
await makeSTXTokenTransfer({
...txOptions,
fee,
}),
network,
);
if (result.error) {
throw new Error(result.reason!);
}
}
type OperationOptions = {
senderKey: string;
nonce: number;
fee?: number;
};
async function publicCall(
operation: Operation.PublicCall,
options: OperationOptions,
) {
const txOptions = {
network,
contractAddress: DEPLOYER_ACCOUNT_ADDRESS(),
contractName: operation.contract,
functionName: operation.function,
functionArgs: operation.args,
nonce: options.nonce,
fee: options.fee,
anchorMode: AnchorMode.Any,
postConditionMode: PostConditionMode.Allow,
senderKey: options.senderKey,
};
const fee = await estimateContractFunctionCall(
await makeContractCall(txOptions),
network,
).catch(() => options.fee);
const result = await broadcastTransaction(
await makeContractCall({
...txOptions,
fee,
}),
network,
);
if (result.error) {
throw new Error(result.reason!);
}
}
export async function getAccountInfo(
address: string,
): Promise<AccountDataResponse> {
const url = `${STACKS_API_URL()}/v2/accounts/${address}?proof=0`;
const res = await fetch(url);
return await res.json().catch(() => null);
}
async function getTransaction(address: string, untilNonce: number) {
let result: Transaction[] = [];
while (result.every(t => t.nonce > untilNonce)) {
const response: AddressTransactionsListResponse = await fetch(
`${STACKS_API_URL()}/extended/v1/address/${address}/transactions?limit=50&offset=${
result.length
}`,
).then(r => r.json());
const newResults = response.results as any[];
if (!newResults.length) {
break;
}
result.push(...newResults);
}
return result.filter(a => a.nonce >= untilNonce);
}

230
codeMap.json Normal file
View File

@@ -0,0 +1,230 @@
{
"377555010": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:108"
},
"377555038": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:115"
},
"377555067": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:123"
},
"377555068": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:124"
},
"377555069": {
"code": "ERR-TOO-MANY-TXINS",
"comment": "clarity-bitcoin.clar:125"
},
"377555160": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:153"
},
"377555163": {
"code": "ERR-TOO-MANY-TXINS",
"comment": "clarity-bitcoin.clar:156"
},
"377555191": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:163"
},
"377555192": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:164"
},
"377555193": {
"code": "ERR-TOO-MANY-TXINS",
"comment": "clarity-bitcoin.clar:165"
},
"377555220": {
"code": "ERR-TOO-MANY-TXINS",
"comment": "clarity-bitcoin.clar:171"
},
"377555225": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:176"
},
"377555226": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:177"
},
"377555227": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:178"
},
"377555285": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:194"
},
"377555287": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:196"
},
"377555964": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:201"
},
"377555965": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:202"
},
"377555966": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:203"
},
"377555972": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:209"
},
"377556028": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:223"
},
"377556030": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:225"
},
"377556213": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:282"
},
"377556214": {
"code": "ERR-VARSLICE-TOO-LONG",
"comment": "clarity-bitcoin.clar:283"
},
"377556215": {
"code": "ERR-TOO-MANY-TXOUTS",
"comment": "clarity-bitcoin.clar:284"
},
"377556216": {
"code": "ERR-TOO-MANY-TXINS",
"comment": "clarity-bitcoin.clar:285"
},
"377558043": {
"code": "ERR-PROOF-TOO-SHORT",
"comment": "clarity-bitcoin.clar:453"
},
"377558046": {
"code": "ERR-PROOF-TOO-SHORT",
"comment": "clarity-bitcoin.clar:456"
},
"377558108": {
"code": "ERR-PROOF-TOO-SHORT",
"comment": "clarity-bitcoin.clar:476"
},
"2090389179": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:15"
},
"2090389183": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:19"
},
"2090389209": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:24"
},
"2090389213": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:28"
},
"2090389239": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:33"
},
"2090389243": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:37"
},
"2090389269": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:42"
},
"2090389273": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:46"
},
"2090389299": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:51"
},
"2090389304": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:56"
},
"2090389367": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:77"
},
"2090389395": {
"code": "ERR-OUT-OF-BOUNDS",
"comment": "clarity-bitcoin.clar:84"
},
"2894909962": {
"code": "ERR-VALIDATOR-ALREADY-REGISTERED",
"comment": "indexer.clar:70"
},
"2894909993": {
"code": "ERR-UNKNOWN-VALIDATOR",
"comment": "indexer.clar:80"
},
"2894910031": {
"code": "ERR-REQUIRED-VALIDATORS",
"comment": "indexer.clar:97"
},
"3842857249": {
"code": "ERR-UNKNOWN-VALIDATOR",
"comment": "indexer.clar:123"
},
"3842857315": {
"code": "ERR-DUPLICATE-SIGNATURE",
"comment": "indexer.clar:147"
},
"3842857316": {
"code": "ERR-ORDER-HASH-MISMATCH",
"comment": "indexer.clar:148"
},
"3842857317": {
"code": "ERR-INVALID-SIGNATURE",
"comment": "indexer.clar:149"
},
"3842857347": {
"code": "ERR-TX-NOT-INDEXED",
"comment": "indexer.clar:158"
},
"3842857404": {
"code": "ERR-PAUSED",
"comment": "indexer.clar:173"
},
"3842857405": {
"code": "ERR-UKNOWN-RELAYER",
"comment": "indexer.clar:174"
},
"3842858185": {
"code": "ERR-TX-ALREADY-INDEXED",
"comment": "indexer.clar:219"
},
"3842858207": {
"code": "ERR-REQUIRED-VALIDATORS",
"comment": "indexer.clar:220"
},
"3842858208": {
"code": "ERR-DOUBLE-SPEND",
"comment": "indexer.clar:221"
},
"3842858209": {
"code": "ERR-FROM-BAL-MISMATCH",
"comment": "indexer.clar:222"
},
"3842858210": {
"code": "ERR-TO-BAL-MISMATCH",
"comment": "indexer.clar:223"
},
"3842858269": {
"code": "ERR-NOT-AUTHORIZED",
"comment": "indexer.clar:240"
}
}

78
dev-stack/devenv Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
PROFILE=${DOCKER_COMPOSE_PROFILE:-'dev'}
export NODE_NO_WARNINGS=1
pushd "$DIR" > /dev/null 2>&1 || exit
if [ ! -e "./wait" ]; then
wget -qO ./wait https://github.com/ufoscout/docker-compose-wait/releases/download/2.12.0/wait
chmod +x ./wait
fi
popd > /dev/null 2>&1 || exit
_docker_compose() {
pushd "$DIR" >/dev/null 2>&1 || exit
docker-compose --profile $PROFILE --project-name brc20-indexer -f stacks-blockchain/docker-compose.yml $@
popd >/dev/null 2>&1 || exit
}
read_docker_compose() {
yq e "$@" dev-stack/stacks-blockchain/docker-compose.yml
}
echo_ports() {
echo postgres: "$(read_docker_compose .services.postgres.ports[0] | cut -d: -f1)"
echo stacks-blockchain-api: "$(read_docker_compose .services.stacks-blockchain-api.ports[0] | cut -d: -f1)"
echo stacks-blockchain: "$(read_docker_compose .services.stacks-blockchain.ports[0] | cut -d: -f1)"
echo stacks-blockchain-explorer: "$(read_docker_compose .services.stacks-blockchain-explorer.ports[0] | cut -d: -f1)"
}
main() {
local cmd=$1
case $cmd in
up)
shift
_docker_compose up $@
echo_ports
;;
upd)
_docker_compose up -d
echo_ports
;;
down)
_docker_compose down $@
;;
logs)
_docker_compose logs -f --tail=100
;;
clean)
_docker_compose down -v -t 0
;;
reset)
_docker_compose down -v -t 0
_docker_compose up -d
echo_ports
echo -n "Waiting for stacks blockchain node"
for _ in $(seq 1 999); do
echo -n .
if curl -so /dev/null http://localhost:$(read_docker_compose .services.stacks-blockchain.ports[0] | cut -d: -f1); then
echo
echo 'stacks blockchain node started'
exit 0
fi
sleep 0.5
done
;;
echo)
echo_ports
;;
*)
echo "Usage: $0 {up|down|logs|clean|reset}"
exit 1
;;
esac
}
main $@

View File

@@ -0,0 +1,33 @@
[node]
working_dir = "/root/stacks-node/data"
rpc_bind = "0.0.0.0:20443"
p2p_bind = "0.0.0.0:20444"
wait_time_for_microblocks = 1000
use_test_genesis_chainstate = true
enable_puppet_mode = true
[[events_observer]]
endpoint = "stacks-blockchain-api:3700"
retry_count = 255
events_keys = ["*"]
[burnchain]
chain = "bitcoin"
mode = "mocknet"
commit_anchor_block_within = 0
[[mstx_balance]]
address = "ST19BH99Z7P8FSJ58EYPZ13CJJNYHC6GVMMM2T1B3"
amount = 100_000_000_000_000
[[mstx_balance]]
address = "STP7HH9H64RQH870ZB9JJWE212QB0HJ1FN5GSGTQ"
amount = 100_000_000_000_000
[connection_options]
public_ip_address = "127.0.0.1:20444"
read_only_call_limit_write_length = 15000000
read_only_call_limit_read_length = 100000000
read_only_call_limit_write_count = 15500
read_only_call_limit_read_count = 15500
read_only_call_limit_runtime = 5000000000

View File

@@ -0,0 +1,86 @@
version: '3.8'
services:
postgres:
image: postgres:15.4-alpine
container_name: brc20_indexer_stacks_pg
command: postgres -c 'max_connections=1000'
profiles: ['dev', 'ci']
shm_size: 1gb
ports:
- '19432:5432'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 4xQUdohS4oIn8pKW
POSTGRES_DB: stacks_blockchain_api
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
stacks-blockchain-api:
image: bestmike007/stacks-blockchain-api:7.3.0
container_name: brc20_indexer_stacks_api
profiles: ['dev', 'ci']
command: sh -c "/wait && node ./lib/index.js"
ports:
- '19999:3999'
environment:
WAIT_HOSTS: 'postgres:5432'
PG_HOST: postgres
PG_PORT: 5432
PG_USER: postgres
PG_PASSWORD: 4xQUdohS4oIn8pKW
PG_DATABASE: stacks_blockchain_api
PG_SCHEMA: stacks_blockchain_api
STACKS_CORE_EVENT_PORT: '3700'
STACKS_CORE_EVENT_HOST: http://0.0.0.0
STACKS_BLOCKCHAIN_API_PORT: '3999'
STACKS_BLOCKCHAIN_API_HOST: 0.0.0.0
STACKS_CORE_RPC_HOST: stacks-blockchain
STACKS_CORE_RPC_PORT: '20443'
STACKS_CHAIN_ID: '0x80000000'
NODE_ENV: development
STACKS_API_ENABLE_FT_METADATA: 1
STACKS_API_ENABLE_NFT_METADATA: 1
volumes:
- $PWD/wait:/wait
stacks-blockchain:
image: bestmike007/stacks-blockchain:alex-v20230808
container_name: brc20_indexer_stacks_node
profiles: ['dev', 'ci']
command: sh -c "/wait && stacks-node start --config=/app/config/Stacks.toml"
ports:
- '19443:20443'
- '19445:20445'
environment:
WAIT_BEFORE: 3
WAIT_AFTER: 3
WAIT_HOSTS: 'stacks-blockchain-api:3700'
NOP_BLOCKSTACK_DEBUG: 1
XBLOCKSTACK_DEBUG: 1
RUST_BACKTRACE: 1
STACKS_CHAIN_ID: '0x80000000'
V2_POX_MIN_AMOUNT_USTX: 90000000260
STACKS_CORE_RPC_HOST: stacks-blockchain
STACKS_CORE_RPC_PORT: 20443
STACKS_API_ENABLE_FT_METADATA: 1
STACKS_API_ENABLE_NFT_METADATA: 1
STACKS_NODE_PUPPET_MODE: 'true'
volumes:
- stacks_blockchain_chaindata:/root/stacks-node/data
- $PWD/stacks-blockchain/config:/app/config
- $PWD/wait:/wait
stacks-blockchain-explorer:
image: hirosystems/explorer:1.39.0
container_name: brc20_indexer_stacks_explorer
profiles: ['dev']
ports:
- '19000:3000'
extra_hosts:
- 'gateway.docker.internal:host-gateway'
environment:
MAINNET_API_SERVER: http://gateway.docker.internal:18999
NEXT_PUBLIC_MAINNET_API_SERVER: ${PUBLIC_MAINNET_API_SERVER:-http://gateway.docker.internal:18999}
NEXT_PUBLIC_MAINNET_ENABLED: 'true'
NODE_ENV: development
volumes:
pgdata: {}
stacks_blockchain_chaindata: {}

View File

@@ -0,0 +1 @@
CREATE SCHEMA stacks_blockchain_api;

BIN
dev-stack/wait Executable file

Binary file not shown.

1
env/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.current_project

12
env/local/bootstrap/config.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"DEPLOYER_ACCOUNT_ADDRESS": "ST19BH99Z7P8FSJ58EYPZ13CJJNYHC6GVMMM2T1B3",
"DEPLOYER_ACCOUNT_SECRETKEY": "c186582e7ec5c5febc121f73db95cfd90a16a80f53869a894cf934c828f1da5601",
"STACKS_API_URL": "http://localhost:19999",
"STACKS_PUPPET_URL": "http://localhost:19445/puppet/v1",
"USER_ACCOUNTS": [
{
"address": "STP7HH9H64RQH870ZB9JJWE212QB0HJ1FN5GSGTQ",
"senderKey": "de9e95d04469551c4eb7a0127155e51024f0e4ea8b1ae4649de227180e85d1c801"
}
]
}

1061
env/mo vendored Executable file

File diff suppressed because it is too large Load Diff

53
env/use.sh vendored Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -eo pipefail
BASE="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )"
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && cd .. && pwd )"
pushd "$BASE"/../
main() {
local ENV="$1"
if [ "$ENV" = "base" -o ! -d "$ROOT/env/$ENV" ]; then
echo unkown env "$ENV"
exit 1
fi
echo preparing env $ENV
export ENV
processEnvFolder "$ROOT/env/base"
processEnvFolder "$ROOT/env/$ENV"
echo "$ENV" > "$BASE"/.current_project
echo switched to env $ENV
}
processEnvFolder() {
if [ -d "$1" ]
then
pushd "$1"
FILES=$(find . -type f)
popd
for f in $FILES
do
writeTemplateFile "$1" "$f"
done
fi
}
writeTemplateFile() {
SOURCE_PATH=$1
FILE_PATH=$2
FILE_DIR=$(dirname "${FILE_PATH}")
mkdir -p "$FILE_DIR"
rm -f "$ROOT"/"$FILE_PATH"
"$BASE"/mo "$SOURCE_PATH"/"$FILE_PATH" > "$ROOT"/"$FILE_PATH"
chmod 400 "$ROOT"/"$FILE_PATH"
}
if [ -n "${1-}" ]
then
main "$1"
else
printf './use.sh ENV \nexample: ./use.sh dev'
fi

View File

@@ -1,16 +1,34 @@
{
"private": true,
"scripts": {
"use": "./env/use.sh",
"devenv": "./dev-stack/devenv",
"gen:list": "yarn ts-node --swc bootstrap/contracts/generateContractList.ts",
"deploy": "yarn ts-node --swc bootstrap/deploy.ts",
"gen": "yarn ts-node --swc bootstrap/contracts/generateContracts.ts",
"setup": "yarn ts-node --swc bootstrap/setup.ts",
"faucet": "yarn ts-node --swc bootstrap/faucet.ts",
"reset-dev": "yarn devenv reset && yarn gen:list && yarn deploy && yarn gen && yarn setup"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@noble/hashes": "^1.3.1",
"@stacks/keychain": "^4.3.8",
"@stacks/network": "^6.5.5",
"@stacks/stacks-blockchain-api-types": "^7.3.0",
"@stacks/transactions": "^6.7.0",
"bignumber.js": "^9.1.1",
"bitcoinjs-lib": "^6.1.3",
"clarity-codegen": "^0.2.2",
"got-cjs": "^12.5.4",
"lodash": "^4.17.21",
"micro-btc-signer": "^0.4.2",
"micro-stacks": "^1.2.1",
"tsx": "^3.12.7"
},
"devDependencies": {
"@swc/core": "^1.3.73",
"@types/lodash": "^4.14.197",
"@types/node": "^20.4.5",
"prettier": "^3.0.0",
"prettier-plugin-organize-imports": "^3.2.3",

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"strict": true,
"module": "commonjs",
"noEmit": true,
"forceConsistentCasingInFileNames": true,
"removeComments": true,
"noImplicitReturns": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"declaration": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"types": ["node"]
},
"exclude": ["node_modules"]
}