Skip to main content

Bulletin board API implementation

이 튜토리얼에서는 게시판 컨트랙트 상호작용을 위한 재사용 가능한 추상화 계층을 제공하는 API 패키지를 구현하는 방법을 설명합니다. 이 패키지는 CLI와 브라우저 기반 UI 간에 공유할 수 있습니다.

Prerequisites

시작하기 전에 게시판 CLI 설정을 완료하고 루트 의존성을 설치했는지 확인하세요.

Create the API directory

루트에서 API 구조를 생성합니다:

mkdir -p api/src/utils
cd api

Configure the API package

api/package.json을 생성합니다:

{
"name": "@midnight-ntwrk/bboard-api",
"version": "0.1.0",
"author": "IOG",
"license": "MIT",
"private": true,
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "rm -rf dist && tsc",
"ci": "npm run typecheck && npm run lint && npm run build",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit"
}
}

Configure TypeScript

api/tsconfig.json을 생성합니다:

{
"include": ["src/**/*.ts", "src/test/jest.setup.ts"],
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"lib": ["ESNext", "DOM"],
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strict": true,
"isolatedModules": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}

Implement utility functions

api/src/utils/index.ts를 생성합니다:

api/src/utils/index.ts
/**
* 유틸리티 함수를 제공합니다.
*
* @module
*/

/**
* 임의로 생성된 바이트 시리즈를 포함하는 버퍼를 생성합니다.
*
* @param length 생성할 바이트 수.
* @returns `length`만큼의 임의 바이트를 나타내는 `Uint8Array`.
*/
export const randomBytes = (length: number): Uint8Array => {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
};

이 유틸리티 함수는 Web Crypto API를 사용하여 암호학적으로 안전한 난수 바이트를 생성합니다. 게시판은 이 함수를 사용하여 사용자의 비밀 키를 생성합니다.

Define common types

api/src/common-types.ts를 생성합니다:

api/src/common-types.ts
import { type MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import { type FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { State, BBoardPrivateState, Contract, Witnesses } from '../../contract/src/index';

export const bboardPrivateStateKey = 'bboardPrivateState';
export type PrivateStateId = typeof bboardPrivateStateKey;

export type PrivateStates = {
readonly bboardPrivateState: BBoardPrivateState;
};

export type BBoardContract = Contract<BBoardPrivateState, Witnesses<BBoardPrivateState>>;

export type BBoardCircuitKeys = Exclude<keyof BBoardContract['impureCircuits'], number | symbol>;

export type BBoardProviders = MidnightProviders<BBoardCircuitKeys, PrivateStateId, BBoardPrivateState>;

export type DeployedBBoardContract = FoundContract<BBoardContract>;

export type BBoardDerivedState = {
readonly state: State;
readonly sequence: bigint;
readonly message: string | undefined;
readonly isOwner: boolean;
};

이러한 타입 정의는 복잡한 제네릭 타입에 대한 별칭을 생성합니다. BBoardDerivedState는 공개 ledger 상태와 계산된 소유권 정보를 결합합니다. isOwner 필드는 ledger의 소유자 commitment와 사용자의 비밀 키를 비교하여 현재 사용자가 메시지를 게시했는지 여부를 결정합니다.

Implement the BBoardAPI class

BBoardAPI 클래스는 게시판 스마트 컨트랙트와 상호작용하기 위한 고수준 인터페이스를 제공합니다. 컨트랙트 배포, 상태 관리 및 트랜잭션 제출을 처리하면서 실시간 업데이트를 위한 반응형 상태 observable을 노출합니다.

api/src/index.ts를 생성하고 다음 섹션을 추가합니다.

Import required packages

api/src/index.ts
import * as BBoard from '../../contract/src/managed/bboard/contract/index.js';

import { type ContractAddress, convertFieldToBytes } from '@midnight-ntwrk/compact-runtime';
import { type Logger } from 'pino';
import {
type BBoardDerivedState,
type BBoardContract,
type BBoardProviders,
type DeployedBBoardContract,
bboardPrivateStateKey,
} from './common-types.js';
import { CompiledBBoardContractContract } from '../../contract/src/index';
import * as utils from './utils/index.js';
import { deployContract, findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
import { combineLatest, map, tap, from, type Observable } from 'rxjs';
import { toHex } from '@midnight-ntwrk/midnight-js-utils';
import { BBoardPrivateState, createBBoardPrivateState } from '@midnight-ntwrk/bboard-contract';

Define the API interface

api/src/index.ts
export interface DeployedBBoardAPI {
readonly deployedContractAddress: ContractAddress;
readonly state$: Observable<BBoardDerivedState>;

post: (message: string) => Promise<void>;
takeDown: () => Promise<void>;
}

이 인터페이스는 컨트랙트 주소, 반응형 상태 observable, 그리고 메시지 게시 및 제거를 위한 메서드를 노출합니다.

Implement the constructor and state observable

BBoardAPI 클래스 생성자는 private으로, 인스턴스가 정적 deploy 또는 join 메서드를 통해서만 생성되도록 합니다.

api/src/index.ts
export class BBoardAPI implements DeployedBBoardAPI {
private constructor(
public readonly deployedContract: DeployedBBoardContract,
providers: BBoardProviders,
private readonly logger?: Logger,
) {
this.deployedContractAddress = deployedContract.deployTxData.public.contractAddress;
this.state$ = combineLatest(
[
providers.publicDataProvider.contractStateObservable(this.deployedContractAddress, { type: 'latest' }).pipe(
map((contractState) => BBoard.ledger(contractState.data)),
tap((ledgerState) =>
logger?.trace({
ledgerStateChanged: {
ledgerState: {
...ledgerState,
state: ledgerState.state === BBoard.State.OCCUPIED ? 'occupied' : 'vacant',
owner: toHex(ledgerState.owner),
},
},
}),
),
),
from(providers.privateStateProvider.get(bboardPrivateStateKey) as Promise<BBoardPrivateState>),
],
(ledgerState, privateState) => {
const hashedSecretKey = BBoard.pureCircuits.publicKey(
privateState.secretKey,
convertFieldToBytes(32, ledgerState.sequence, 'api/src/index.ts'),
);

return {
state: ledgerState.state,
message: ledgerState.message.value,
sequence: ledgerState.sequence,
isOwner: toHex(ledgerState.owner) === toHex(hashedSecretKey),
};
},
);
}

readonly deployedContractAddress: ContractAddress;

readonly state$: Observable<BBoardDerivedState>;

반응형 상태 observable은 RxJS combineLatest를 사용하여 두 데이터 스트림을 병합합니다: indexer로부터의 공개 ledger 상태와 로컬 저장소의 private 상태입니다. private 비밀 키를 해싱한 뒤 온체인 소유자 필드와 비교하여 isOwner 플래그를 계산합니다. 이 값을 통해 해당 사용자가 takeDown()으로 메시지를 제거할 수 있는지 결정합니다.

Implement the post method

api/src/index.ts
  async post(message: string): Promise<void> {
this.logger?.info(`postingMessage: ${message}`);

const txData = await this.deployedContract.callTx.post(message);

this.logger?.trace({
transactionAdded: {
circuit: 'post',
txHash: txData.public.txHash,
blockHeight: txData.public.blockHeight,
},
});
}

post 메서드는 스마트 컨트랙트의 post circuit를 호출하고, ZK proof가 생성되고 트랜잭션이 확인될 때까지 대기한 후 트랜잭션 세부 정보를 로깅합니다.

Implement the takeDown method

api/src/index.ts
  async takeDown(): Promise<void> {
this.logger?.info('takingDownMessage');

const txData = await this.deployedContract.callTx.takeDown();

this.logger?.trace({
transactionAdded: {
circuit: 'takeDown',
txHash: txData.public.txHash,
blockHeight: txData.public.blockHeight,
},
});
}

takeDown 메서드는 스마트 컨트랙트의 takeDown circuit를 호출하며, 소유권 proof가 필요합니다. 호출자의 해싱된 비밀 키가 온체인 소유자 필드와 일치하는 경우에만 성공할 수 있습니다.

Implement the deploy method

api/src/index.ts
  static async deploy(providers: BBoardProviders, logger?: Logger): Promise<BBoardAPI> {
logger?.info('deployContract');

const deployedBBoardContract = await deployContract(providers, {
compiledContract: CompiledBBoardContractContract,
privateStateId: bboardPrivateStateKey,
initialPrivateState: await BBoardAPI.getPrivateState(providers),
});

logger?.trace({
contractDeployed: {
finalizedDeployTxData: deployedBBoardContract.deployTxData.public,
},
});

return new BBoardAPI(deployedBBoardContract, providers, logger);
}

deploy 메서드는 블록체인에 새로운 게시판 컨트랙트 인스턴스를 생성합니다. 배포된 각 컨트랙트는 다른 사용자가 게시판에 참여하고 상호작용할 수 있도록 고유한 온체인 주소를 받습니다.

Implement the join method

api/src/index.ts
  static async join(providers: BBoardProviders, contractAddress: ContractAddress, logger?: Logger): Promise<BBoardAPI> {
logger?.info({
joinContract: {
contractAddress,
},
});

const deployedBBoardContract = await findDeployedContract<BBoardContract>(providers, {
contractAddress,
compiledContract: CompiledBBoardContractContract,
privateStateId: bboardPrivateStateKey,
initialPrivateState: await BBoardAPI.getPrivateState(providers),
});

logger?.trace({
contractJoined: {
finalizedDeployTxData: deployedBBoardContract.deployTxData.public,
},
});

return new BBoardAPI(deployedBBoardContract, providers, logger);
}

join 메서드는 기존에 배포된 게시판 컨트랙트에 연결합니다. 연결 후 공개 상태를 읽을 수 있으며, 현재 메시지의 게시자라면 비밀 키를 사용하여 메시지를 제거할 수 있습니다.

Implement private state helper

api/src/index.ts
  private static async getPrivateState(providers: BBoardProviders): Promise<BBoardPrivateState> {
const existingPrivateState = await providers.privateStateProvider.get(bboardPrivateStateKey);
return existingPrivateState ?? createBBoardPrivateState(utils.randomBytes(32));
}
}

이 헬퍼는 기존 private 상태를 가져오거나, 존재하지 않는 경우 새로운 32바이트 난수 비밀 키를 생성합니다. private 상태는 애플리케이션 재시작 시에도 유지되어 사용자가 향후 세션에서 게시한 메시지의 소유권을 proof할 수 있습니다.

Export utilities and types

api/src/index.ts
export * as utils from './utils/index.js';

export * from './common-types.js';

이 내보내기는 API의 유틸리티 함수와 TypeScript 타입을 CLI 애플리케이션 및 API 모듈의 다른 소비자에게 제공합니다.

Build the API package

API 패키지를 빌드합니다:

npm run build

이 명령은 TypeScript를 JavaScript로 컴파일하고 dist 디렉터리에 타입 정의를 생성합니다.

Next steps

이 API를 사용하는 명령줄 인터페이스를 구축하려면 CLI 구현 튜토리얼을 계속 진행하세요.