Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fd53da8
feat: compiling version
nicolasLuduena Jul 23, 2026
698cd73
chore: update compose yml files
nicolasLuduena Jul 23, 2026
ecc4b35
chore: docs with research results for contract improvements
nicolasLuduena Jul 23, 2026
5e4f1d0
feat: validate sponsored composite transactions
nicolasLuduena Jul 27, 2026
85892e5
feat: sponsorship verification plan updated
nicolasLuduena Jul 27, 2026
e73b85e
feat: verification separation & public API stabilization
nicolasLuduena Jul 27, 2026
3336b82
feat: add round-robin sponsorship reward split
nicolasLuduena Jul 27, 2026
87f3bbb
feat: add Midnight eligibility sponsor service
nicolasLuduena Jul 28, 2026
bdf8db8
chore: fix lint and formatting
nicolasLuduena Jul 28, 2026
0001033
feat: create experimenting package
nicolasLuduena Jul 28, 2026
bd73803
feat: provider config generic over contract
nicolasLuduena Jul 28, 2026
37b48f4
feat: extract experiment harness
nicolasLuduena Jul 28, 2026
24465c9
feat: run experiment on empty network
nicolasLuduena Jul 28, 2026
90eb35e
feat: improve docs in sponsorship test
nicolasLuduena Jul 29, 2026
efa7866
feat: update protocol verfication experiment
nicolasLuduena Jul 29, 2026
2fc8386
fix: set contract address for beneficiary provider
nicolasLuduena Jul 29, 2026
849e678
feat: rerun sponsorship experiment from scratch
nicolasLuduena Jul 29, 2026
9f82320
chore: remove agentic artifacts
nicolasLuduena Jul 29, 2026
ecb53ac
chore: prettier
nicolasLuduena Jul 29, 2026
7febd11
fix: node version in CI
nicolasLuduena Jul 29, 2026
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/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: '24'
cache: 'pnpm'

- name: Install dependencies
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ managed/
tsconfig.tsbuildinfo

logs/
*.sqlite

**/midnight-level-db/
**/*-midnight/
Expand All @@ -18,4 +19,4 @@ logs/
.eslintcache

# Prettier
.prettiercache
.prettiercache
36 changes: 22 additions & 14 deletions VERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
# Component Versions

2026-04-24
2026-07-23

## Versions in Use

- Compact compiler: 0.30.0
- Indexer: 4.0.0
- Proof server: 8.0.2
- Midnight node: 0.22.0
- Compact devtools: 0.5.1
- Compact compiler: 0.31.1
- Compact language: 0.23.0
- Compact runtime: 0.16.0
- Compact JS: 2.5.1
- Midnight.js: 4.1.1
- On-chain runtime: 3.0.0
- Indexer (standalone devnet): 4.2.1
- Proof server: 8.1.0
- Midnight node (local devnet): 0.22.5
- Node.js: 24.13.1

## Project dependencies

````json
"dependencies": {
"@midnight-ntwrk/compact-js": "2.5.0",
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/compact-js": "2.5.1",
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/midnight-js-contracts": "4.1.1",
"@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-network-id": "4.1.1",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-types": "4.1.1",
"@midnight-ntwrk/onchain-runtime-v3": "3.0.0",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0-rc.0",
"@midnight-ntwrk/wallet-sdk-capabilities": "3.2.0-rc.0",
"@midnight-ntwrk/wallet-sdk-abstractions": "2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-contracts": "4.1.1",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0-rc.0",
"@midnight-ntwrk/zswap": "4.0.0",
"@midnight-sentinel/api": "workspace:*",
Expand Down
198 changes: 167 additions & 31 deletions apps/cli/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { SentinelContract } from '@midnight-sentinel/api';
import { sponsorshipAllowlistHash } from '@midnight-sentinel/api/sponsorship';
import {
createHttpMidnightRegistrationProvider,
enrollmentSigningBytes,
type EnrollmentPayload,
type SignedEnrollment,
} from '@midnight-sentinel/api/sponsorship/eligibility';
import {
createMidnightSponsorSponsorshipApi,
dustPublicKeyToBytes,
nativeNightSponsorshipConfig,
} from '@midnight-sentinel/api/sponsorship/midnight';
import { configureProviders } from '@midnight-sentinel/contract/providers';
import {
getBalancesAndAddresses,
getNetworkId,
printBalances,
type WalletContext,
} from '@midnight-sentinel/wallet';
Expand All @@ -12,54 +25,73 @@ import { circuitMenu, contractMenu } from './menus.js';
async function handleCircuits(
contract: SentinelContract,
_walletDetails: { seed: string; privateStateStoreName: string },
walletCtx: WalletContext,
rli: Interface
_walletCtx: WalletContext,
rli: Interface,
config: Config
) {
while (true) {
const choice = await rli.question(circuitMenu);
switch (choice) {
case '1':
try {
const key = walletCtx.shieldedSecretKeys.coinPublicKey;
const amount = await rli.question('Enter the amount you would like to delegate: ');
await contract.delegate(key, BigInt(amount));
} catch (e) {
console.log('Error delegating: ', e);
}
await contract.getCurrentState();
break;
case '2':
try {
await contract.redeemRewards();
await contract.setSponsorshipEnabled(false);
} catch (e) {
console.log('Error redeeming rewards: ', e);
console.log('Error pausing sponsorship: ', e);
}
return;
break;
case '3':
try {
await contract.withdraw();
await contract.setSponsorshipEnabled(true);
} catch (e) {
console.log('Error wthdrawing: ', e);
console.log('Error resuming sponsorship: ', e);
}
break;
case '4':
try {
const amount = await rli.question(
'Enter the amount you would like to deposit as rewards: '
);
// TODO: wire up to wallet
await contract.depositRewards(
BigInt(amount),
new Uint8Array(32).fill(0),
new Uint8Array(32).fill(0)
);
} catch (e) {
console.log('Error depositing rewards: ', e);
const serviceUrl =
(await rli.question(`Eligibility service URL [${config.eligibilityService}]: `)) ||
config.eligibilityService;
const raw = await rli.question('Signed enrollment JSON: ');
const enrollment = JSON.parse(raw) as SignedEnrollment;
const response = await fetch(`${serviceUrl.replace(/\/$/, '')}/v1/enrollments`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(enrollment),
});
console.log(await response.text());
if (!response.ok) {
throw new Error(`Enrollment service returned HTTP ${response.status}`);
}
} catch (error) {
console.error('Enrollment failed:', error);
}
return;
break;
case '5':
await contract.getCurrentState();
try {
const identity = (await rli.question('Delegator identity (32-byte hex): ')).replace(
/^0x/,
''
);
await contract.removeDelegator(Uint8Array.from(Buffer.from(identity, 'hex')));
} catch (error) {
console.error('Removal failed:', error);
}
break;
case '6':
try {
const authority = (await rli.question('New operator authority (32-byte hex): ')).replace(
/^0x/,
''
);
await contract.rotateEligibilityOperator(Uint8Array.from(Buffer.from(authority, 'hex')));
} catch (error) {
console.error('Rotation failed:', error);
}
break;
case '7':
console.log('Exiting...');
return;
default:
Expand Down Expand Up @@ -87,7 +119,35 @@ export async function runCli(
config,
walletDetails.privateStateStoreName
);
contract = await SentinelContract.deploy(providers);
const sponsorShare = BigInt((await rli.question('Sponsor NIGHT share [1]: ')) || '1');
const delegatorShare = BigInt((await rli.question('Delegator NIGHT share [1]: ')) || '1');
const minimumRegisteredNight = BigInt(
(await rli.question('Minimum registered NIGHT [1]: ')) || '1'
);
const operatorHex = (
await rli.question('Eligibility operator authority key (32-byte hex): ')
)
.trim()
.replace(/^0x/, '');
if (!/^[0-9a-fA-F]{64}$/.test(operatorHex)) {
throw new Error('Eligibility operator key must be 32-byte hex');
}
const targetAddress = (
await rli.question('Initial allowed target contract address: ')
).trim();
const targetEntryPoint = (await rli.question('Initial allowed target circuit: ')).trim();
const policyHash = sponsorshipAllowlistHash([
{ address: targetAddress, entryPoint: targetEntryPoint },
]);
contract = await SentinelContract.deploy(
providers,
nativeNightSponsorshipConfig(walletCtx, policyHash, {
sponsorShare,
delegatorShare,
minimumRegisteredNight,
initialEligibilityOperator: Uint8Array.from(Buffer.from(operatorHex, 'hex')),
})
);

console.log(
`[Contract Address]: ${contract.deployedContract?.deployTxData.public.contractAddress}`
Expand All @@ -113,8 +173,50 @@ export async function runCli(
break;
case '3':
try {
const raw = await rli.question('Enter the raw transaction recipe: ');
await SentinelContract.zswapSponsor(walletCtx, raw);
const sentinelAddress = (
await rli.question('Sentinel sponsorship contract address: ')
).trim();
const targetAddress = (await rli.question('Allowed target contract address: ')).trim();
const targetEntryPoint = (await rli.question('Allowed target circuit: ')).trim();
const maxFee = BigInt(await rli.question('Maximum DUST fee: '));
const sponsorDustAddress = (await rli.question('Campaign sponsor DUST address: ')).trim();
const eligibilityService =
(await rli.question(`Eligibility service URL [${config.eligibilityService}]: `)) ||
config.eligibilityService;
const raw = (await rli.question('Prepared transaction (hex): ')).trim();
const allowedTargets = [{ address: targetAddress, entryPoint: targetEntryPoint }];
const providers = await configureProviders(
walletCtx,
config,
walletDetails.privateStateStoreName
);
const sponsorshipApi = createMidnightSponsorSponsorshipApi({
policy: {
sentinelAddress,
sponsorId: dustPublicKeyToBytes(walletCtx.dustSecretKey.publicKey),
sponsorDustAddress,
registrationProvider: createHttpMidnightRegistrationProvider(eligibilityService),
policyHash: sponsorshipAllowlistHash(allowedTargets),
allowedTargets,
minTtlMs: 30_000,
maxTtlMs: 65 * 60 * 1_000,
maxFee,
},
sentinelProviders: providers,
sponsor: walletCtx,
});
const result = await sponsorshipApi.sponsorAndSubmit({
transaction: Uint8Array.from(Buffer.from(raw, 'hex')),
});
console.log(
JSON.stringify({
txId: result.txId,
status: result.status,
feeEstimate: result.feeEstimate.toString(),
targetAddress: result.targetAddress,
targetEntryPoint: result.targetEntryPoint,
})
);
} catch (e) {
console.log('Error sponsoring DUST: ', e);
}
Expand All @@ -138,13 +240,47 @@ export async function runCli(
break;
}
case '6':
try {
const sentinelAddress = (await rli.question('Sentinel contract address: ')).trim();
const sponsorDustAddress = (await rli.question('Campaign sponsor DUST address: ')).trim();
const nonce = (await rli.question('Enrollment nonce [1]: ')) || '1';
const expiresInMinutes = Number(
(await rli.question('Expires in minutes [60]: ')) || '60'
);
if (!Number.isFinite(expiresInMinutes) || expiresInMinutes <= 0) {
throw new Error('Expiry must be a positive number of minutes');
}
const payload: EnrollmentPayload = {
version: 1,
network: getNetworkId(),
sentinelAddress,
sponsorDustAddress,
nightRewardAddress: walletCtx.unshieldedKeystore.getBech32Address().toString(),
nightVerificationKey: walletCtx.unshieldedKeystore.getPublicKey(),
shieldedCoinPublicKey: walletCtx.shieldedSecretKeys.coinPublicKey,
shieldedEncryptionPublicKey: walletCtx.shieldedSecretKeys.encryptionPublicKey,
nonce: BigInt(nonce).toString(),
expiresAt: new Date(Date.now() + expiresInMinutes * 60_000).toISOString(),
};
const enrollment: SignedEnrollment = {
payload,
signature: walletCtx.unshieldedKeystore.signData(enrollmentSigningBytes(payload)),
};
console.log(JSON.stringify(enrollment));
} catch (error) {
console.error('Could not create enrollment:', error);
}
break;
case '7':
console.log('Exiting...');
return;
default:
console.error('Invalid choice');
continue;
}

if (contract) await handleCircuits(contract, walletDetails, walletCtx, rli);
if (contract) {
await handleCircuits(contract, walletDetails, walletCtx, rli, config);
}
}
}
18 changes: 10 additions & 8 deletions apps/cli/src/cli/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ export const contractMenu: string = `
${DIVIDER}
[1] Deploy a new contract
[2] Join an existing contract
[3] (Admin) Submit transaction sponsoring DUST
[3] (Sponsor) Inspect, add only DUST, and submit
[4] Start ZSwap to request DUST sponsorship
[5] Get balances
[6] Exit
[6] Create signed delegator enrollment
[7] Exit
${DIVIDER}
`;

export const circuitMenu: string = `
${DIVIDER}
[1] Delegate NIGHT
[2] Redeem rewards
[3] (Admin) Withdraw NIGHTs
[4] (Admin) Deposit rewards
[5] Get contract state
[6] Exit
[1] Get sponsorship state
[2] (Owner) Pause sponsorship
[3] (Owner) Resume sponsorship
[4] (Operator) Verify and enroll signed request
[5] (Operator) Remove delegator
[6] (Owner) Rotate eligibility operator
[7] Exit
${DIVIDER}
`;
2 changes: 2 additions & 0 deletions apps/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Config {
readonly indexerWS: string;
readonly node: string;
readonly proofServer: string;
readonly eligibilityService: string;
}

export const currentDir = path.resolve(new URL(import.meta.url).pathname, '..');
Expand All @@ -30,4 +31,5 @@ export class StandaloneConfig implements Config {
indexerWS = 'ws://127.0.0.1:8088/api/v3/graphql/ws';
node = 'http://127.0.0.1:9944';
proofServer = 'http://127.0.0.1:6300';
eligibilityService = 'http://127.0.0.1:8089';
}
Loading
Loading