Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: yarn docs

- name: Deploy docs
uses: JamesIves/github-pages-deploy-action@v4
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
folder: ./docs
25 changes: 24 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main

permissions:
contents: write
contents: read

jobs:
eslint:
Expand Down Expand Up @@ -59,4 +59,27 @@ jobs:

docs:
needs: test
permissions:
contents: write
uses: ./.github/workflows/docs.yml

audit:
name: Audit production dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"

- name: Install project dependencies
run: yarn install --frozen-lockfile

- name: Fail on high or critical advisories
run: yarn audit-ci
21 changes: 21 additions & 0 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,24 @@ jobs:
run: yarn test --coverage
env:
TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }}

audit:
name: Audit production dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"

- name: Install project dependencies
run: yarn install --frozen-lockfile

- name: Fail on high or critical advisories
run: yarn audit-ci
27 changes: 25 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ jobs:

test:
needs: eslint
permissions: write-all
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -60,6 +62,27 @@ jobs:
path: coverage/lcov.info

- name: Code Coverage Report
uses: romeovs/lcov-reporter-action@v0.2.11
uses: romeovs/lcov-reporter-action@90fd2b751de87c16edf5ae1d5878b73755c497b0 # v0.2.11
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

audit:
name: Audit production dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"

- name: Install project dependencies
run: yarn install --frozen-lockfile

- name: Fail on high or critical advisories
run: yarn audit-ci
20 changes: 17 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,32 @@ on:
- 'v*'

permissions:
id-token: write # Required for OIDC
contents: read

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for npm provenance via OIDC
contents: read
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- run: yarn install --frozen-lockfile
- run: npm publish

- name: Install project dependencies
run: yarn install --frozen-lockfile

- name: Audit production dependencies
run: yarn audit-ci

- name: Build
run: yarn build

# Authentication uses npm trusted publishing (OIDC) via `id-token: write`
# above, so no NPM_TOKEN secret is required.
- name: Publish to npm with provenance
run: npm publish --provenance --access public
13 changes: 12 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,18 @@ export default {
// snapshotSerializers: [],

// The test environment that will be used for testing
testEnvironment: 'jsdom',
// Every test in this suite drives the live Partisia testnet: each action
// waits for a transaction to be broadcast, executed and finalized. CI runners
// are slower than a local machine, so the timeout is generous and global
// rather than repeated per test.
testTimeout: 30_000,

// The suite exercises the SDK the way a Node consumer does and talks to the
// testnet over HTTP. Under jsdom, axios picks its XMLHttpRequest adapter,
// whose requests intermittently died with "read ETIMEDOUT"; a hung request
// times the test out mid-broadcast and the next transaction then reuses a
// spent nonce, which the reader node rejects with 400 Bad Request.
testEnvironment: 'node',

// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"repository": "https://github.com/MetaNames/sdk.git",
"author": "Yeboster <marco@yeboster.com>",
"license": "MIT",
"engines": {
"node": ">=20"
},
"files": [
"dist"
],
Expand All @@ -15,7 +18,8 @@
"docs": "typedoc --out docs src",
"format": "eslint --fix src",
"test": "jest -i",
"prepublishOnly": "yarn build"
"prepublishOnly": "yarn build",
"audit-ci": "bash scripts/audit-ci.sh"
},
"dependencies": {
"@ledgerhq/hw-transport": "^6.34.0",
Expand Down Expand Up @@ -50,5 +54,11 @@
},
"publishConfig": {
"directory": "dist"
},
"resolutions": {
"minimatch": "^9.0.7",
"**/ethjs-unit/bn.js": "^4.12.3",
"**/number-to-bn/bn.js": "^4.12.3",
"elliptic": "^6.6.1"
}
}
19 changes: 19 additions & 0 deletions scripts/audit-ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Fail CI on high (8) or critical (16) advisories in the production dependency
# tree. `yarn audit` ignores `--level` for exit-code purposes and instead
# returns a bitmask of the severities it found:
#
# 1 = info 2 = low 4 = moderate 8 = high 16 = critical
#
# so gating requires masking the code rather than trusting a non-zero exit.
set -uo pipefail

yarn audit --groups dependencies
code=$?

if [ $((code & 24)) -ne 0 ]; then
echo "::error::High or critical vulnerabilities found in production dependencies (yarn audit bitmask: $code)"
exit 1
fi

echo "No high or critical vulnerabilities in production dependencies (yarn audit bitmask: $code)"
2 changes: 1 addition & 1 deletion src/repositories/contract-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class ContractRepository implements IContractRepository {
'low': 8_000,
'medium': 40_000,
'high': 50_000,
'extra-high': 100_000,
'extra-high': 200_000,
}

let gas = gasTable.low
Expand Down
45 changes: 33 additions & 12 deletions src/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ export const createTransactionFromPartisiaClient = async (
return buildTransactionResult(rpc, rpcShard, transaction.trxHash)
}

/**
* The nonce comes from a reader node, which trails the chain by a moment: a
* transaction signed right after another one, or from a wallet that is also in
* use elsewhere, can carry a nonce the chain has already spent. The node then
* rejects the broadcast with 400 Bad Request. A rejected transaction never
* reaches the chain, so re-reading the nonce and signing again is safe and
* costs nothing.
*/
const BROADCAST_ATTEMPTS = 3

export const createTransactionFromPrivateKey = async (
rpc: PartisiaAccountClass,
contractAddress: string,
Expand All @@ -125,21 +135,32 @@ export const createTransactionFromPrivateKey = async (
const walletAddress = privateKeyToAccountAddress(privateKey)
const shardId = rpc.deriveShardId(walletAddress)
const url = rpc.getShardUrl(shardId)
const rpcShard = PartisiaRpc({ baseURL: url })

const serializedTransaction = await serializeTransaction(rpc, walletAddress, contractAddress, payload, cost)
let lastError: unknown
for (let attempt = 0; attempt < BROADCAST_ATTEMPTS; attempt++) {
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 1_000 * attempt))

const digest = deriveDigest(
`Partisia Blockchain${isMainnet ? '' : ' Testnet'}`,
serializedTransaction
)
const signature = signTransaction(digest, privateKey)
const trx = getTransactionPayloadData(serializedTransaction, signature)
const serializedTransaction = await serializeTransaction(rpc, walletAddress, contractAddress, payload, cost)

const transactionHash = getTrxHash(digest, signature)
const rpcShard = PartisiaRpc({ baseURL: url })
const digest = deriveDigest(
`Partisia Blockchain${isMainnet ? '' : ' Testnet'}`,
serializedTransaction
)
const signature = signTransaction(digest, privateKey)
const trx = getTransactionPayloadData(serializedTransaction, signature)

const isValid = await rpcShard.broadcastTransaction(trx)
assert(isValid, 'Unknown Error')
const transactionHash = getTrxHash(digest, signature)

return buildTransactionResult(rpc, rpcShard, transactionHash)
try {
const isValid = await rpcShard.broadcastTransaction(trx)
assert(isValid, 'Unknown Error')

return buildTransactionResult(rpc, rpcShard, transactionHash)
} catch (error) {
lastError = error
}
}

throw lastError
}
8 changes: 4 additions & 4 deletions test/domain/action-domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test('run action mint', async () => {
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 15_000)
})

test('run action mint batch', async () => {
const randomActionMint: IActionDomainMint = {
Expand All @@ -30,7 +30,7 @@ test('run action mint batch', async () => {
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 15_000)
})

test('run action renew', async () => {
const randomActionRenew: IActionDomainRenewal = {
Expand All @@ -43,7 +43,7 @@ test('run action renew', async () => {
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 15_000)
})

test('run action transfer', async () => {
const transferParams: IActionDomainTransfer = {
Expand All @@ -55,4 +55,4 @@ test('run action transfer', async () => {
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 15_000)
})
4 changes: 2 additions & 2 deletions test/domain/domain-lookup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ test('lookup domains by owner', async () => {
expect(domain).toHaveProperty('tokenId')
expect(domain).toHaveProperty('parentId')
expect(domain).toHaveProperty('records')
}, 20_000)
})

test('get all domains', async () => {
const domains = await config.sdk.domainRepository.getAll()

expect(domains.length).toBeGreaterThan(0)
expect(domains[0]).toHaveProperty('name')
}, 20_000)
})
6 changes: 3 additions & 3 deletions test/domain/domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const domainName = 'name.mpc'
beforeAll(async () => {
const domain = await config.sdk.domainRepository.find(domainName)
if (!domain) await mintDomain(domainName)
}, 15_000)
})

test('analyze domain', () => {
const domainName = 'sub.name.mpc'
Expand Down Expand Up @@ -61,7 +61,7 @@ test('findByOwner returns the correct domains', async () => {
expect(domain!.owner).toEqual(config.address)
expect(domain!.tld).toEqual('mpc')
expect(domain!.createdAt).toBeInstanceOf(Date)
}, 20_000)
})

test('run action count', async () => {
const count = await config.sdk.domainRepository.count()
Expand All @@ -75,7 +75,7 @@ test('get owners', async () => {
expect(owners).toBeDefined()
expect(owners.length).toBeGreaterThan(0)
expect(owners).toContain(config.address)
}, 20_000)
})

test('toJson', async () => {
const domain = await config.sdk.domainRepository.find(domainName)
Expand Down
4 changes: 2 additions & 2 deletions test/domain/mint-fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ test('mint fees transaction for TEST_COINT', async () => {
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 20_000)
})

test('mint fees transaction for ETH', async () => {
const domainName = 'verycheapfees.mpc'
const { transactionHash, fetchResult } = await config.sdk.domainRepository.approveMintFees(domainName, 'ETH_GOERLI')
const result = await fetchResult

verifyTransactionResult(transactionHash, result)
}, 20_000)
})

test('mint fees amount TEST_COIN', async () => {
const domainName = 'verycheap.mpc'
Expand Down
Loading
Loading