신뢰 통합: 개발자를 위한 Resume Protocol 가이드
Source: Dev.to
번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다. 현재는 소스 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트 전체를 복사해서 보내 주시면 바로 번역해 드릴게요.
Resume Integrator – A Reference Implementation
이전 글에서 Resume Protocol을 소개했습니다. 이 프로토콜은 전문적인 평판을 검증 가능하고, 영혼에 묶이며, 여러분이 직접 소유할 수 있게 설계되었습니다.
하지만 프로토콜은 그것을 활용할 도구가 있어야만 유용합니다.
복잡한 스마트 계약과 일상적인 활용 사이의 격차를 메우기 위해 Resume Integrator를 만들었습니다. 이것은 단순한 스크립트가 아니라, Web3 엔지니어링에서 신뢰성과 우수성을 보여주는 레퍼런스 구현입니다.
프리랜서 마켓플레이스든 대학 인증 포털이든, 핵심 과제는 동일합니다: 풍부한 오프체인 증거(PDF, 이미지 등)를 온체인 진실(불변 원장)과 연결하는 일.
이 가이드에서는 이 통합을 구현하기 위해 선택한 신중한 아키텍처 결정을 단계별로 살펴보겠습니다.
The Engineering Challenge
| Layer | Description |
|---|---|
| Evidence (off‑chain) | 상세 설명, 디자인 포트폴리오, 인증서 등 – 체인에 저장하기엔 비효율적인 대용량 데이터. |
| Truth (on‑chain) | 소유권 및 인증에 대한 암호학적 증명. |
Resume Integrator의 목표는 이 두 요소를 매끄럽게 연결하여, 견고하면서도 사용자 중심적인 시스템을 만드는 것이었습니다.
Source: …
아키텍처 개요
Step 1 – Intent (Metadata) 로 데이터 구조화
우리는 ERC‑721 메타데이터 표준을 따르고, 이를 엄격한 TypeScript 인터페이스로 강제합니다. 데이터 형태를 추측하지 않고, 직접 정의합니다.
// src/types.ts
export interface Attribute {
trait_type: string;
value: string | number | Date;
}
/**
* Standard ERC‑721 Metadata Schema
* Strict typing ensures every credential we mint
* is readable by standard wallets.
*/
export interface CredentialMetadata {
name: string;
description: string;
image: string;
attributes: Attribute[];
}
Step 2 – 스토리지 레이어 (Pinata SDK)
우리는 IPFS를 Pinata SDK를 통해 사용합니다. Pinata는 관리형 서비스의 신뢰성을 제공하면서도 콘텐츠 주소 지정의 탈중앙성을 손상시키지 않습니다.
아래 “Two‑Step” 패턴은 데이터 무결성을 보장합니다:
- 시각적 증명을 먼저 업로드합니다.
- 그 증명의 URI를 메타데이터에 삽입합니다.
// src/storage.ts
/**
* Creates NFT‑compatible metadata for a credential
* and uploads it to IPFS via Pinata.
*
* This function optionally uploads an image first,
* then embeds its IPFS URL into the metadata JSON.
*
* @param input Credential metadata fields
* @returns A public IPFS gateway URL pointing to the metadata JSON
*/
export async function createCredentialMetadata(
input: CredentialMetadataInput
): Promise<string> {
console.log('Authenticating with Pinata...');
await pinata.testAuthentication();
console.log('Pinata authentication successful');
// Will store the IPFS URL of the uploaded image (if any)
let image = '';
// If an image path is provided, upload the image to IPFS first
if (input.imagePath && fs.existsSync(input.imagePath)) {
console.log(`Uploading image: ${input.imagePath}`);
// Read the image file from disk into a buffer
const buffer = fs.readFileSync(input.imagePath);
// Convert the buffer into a File object (Node 18+ compatible)
const file = new File([buffer], 'credential.png', {
type: 'image/png',
});
// Upload the image to Pinata's public IPFS network
const upload = await pinata.upload.public.file(file);
// Construct a gateway‑accessible URL using the returned CID
image = `https://${CONFIG.PINATA_GATEWAY}/ipfs/${upload.cid}`;
console.log(` Image URL: ${image}`);
} else if (input.imagePath) {
console.warn(
`Warning: Image path provided but file not found: ${input.imagePath}`
);
}
// Construct ERC‑721 compatible metadata JSON
const metadata: CredentialMetadata = {
name: input.skillName,
description: input.description,
image,
attributes: [
{ trait_type: 'Recipient', value: input.recipientName },
{ trait_type: 'Endorser', value: input.issuerName },
{
trait_type: 'Date',
value: new Date(
input.endorsementDate.toISOString().split('T')[0]!
),
},
{ trait_type: 'Token Standard', value: 'Soulbound (SBT)' },
],
};
// Upload the metadata JSON to IPFS
console.log('Uploading metadata JSON...');
const result = await pinata.upload.public.json(metadata);
// Return a public gateway URL pointing to the metadata
// This URL can be used directly as a tokenURI on‑chain
return `https://${CONFIG.PINATA_GATEWAY}/ipfs/${result.cid}`;
}
Step 3 – 발행 레이어 (Viem)
나는 Viem을 선택했습니다. 가볍고 타입‑안전하며, 부풀림보다 정확성을 중시하는 내 선호와 잘 맞기 때문입니다.
여기서 가장 중요한 엔지니어링 결정은 확인(confirmations) 대기입니다. 트랜잭션을 브로드캐스트하는 것만으로는 충분하지 않으며, 최종 확정되었는지 확인해야 합니다. 이는 UI 오류를 방지하고 사용자가 자신의 평판이 언제 안전하게 기록됐는지 정확히 알 수 있게 합니다.
// src/contract.ts
/**
* Mint a new endorsement on‑chain
*/
export async function mintEndorsement(
recipient: string,
skill: string,
dataURI: string
): Promise<string> {
if (!CONFIG.CONTRACT_ADDRESS) {
throw new Error('Contract address
단계 4 – 검증 (읽기)
블록체인 상태 변수를 하나씩 조회하는 것은 느리고 비용이 많이 듭니다. 대신 이벤트 로그를 사용합니다. EndorsementMinted 이벤트를 청취함으로써 사용자의 전체 직업 이력을 단일하고 효율적인 쿼리로 재구성할 수 있습니다. 이 접근 방식은 네트워크와 사용자 모두를 배려합니다.
이게 전부입니다! Resume Integrator는 오프체인 증거를 온체인 평판에 연결하는 깔끔하고 프로덕션 준비가 된 방식을 보여주며, 개발자 경험은 즐겁게, 최종 사용자 경험은 매끄럽게 유지합니다. 레포를 포크하고, 실험해 보며, 차세대 자격증 기반 애플리케이션을 구축해 보세요.
src/contract.ts
/**
* Retrieves all endorsements for a given user address.
*
* @param userAddress - The address of the user whose endorsements are being queried.
* @throws Will throw an error if the contract address is not defined in the environment.
* @returns An array of endorsement objects containing tokenId, skill, issuer, and status.
*/
export async function getEndorsementsFor(userAddress: string) {
if (!CONFIG.CONTRACT_ADDRESS) {
throw new Error('Contract Address not set in .env');
}
console.log(`Querying endorsements for ${userAddress}...`);
const logs = await publicClient.getLogs({
address: CONFIG.CONTRACT_ADDRESS,
event: parseAbiItem(
'event EndorsementMinted(uint256 indexed tokenId, address indexed issuer, address indexed recipient, bytes32 skillId, string skill, uint8 status)'
),
args: {
recipient: userAddress as Hex,
},
fromBlock: 'earliest',
});
return logs.map((log) => ({
tokenId: log.args.tokenId,
skill: log.args.skill,
issuer: log.args.issuer,
status: log.args.status === 1 ? 'Active' : 'Pending',
}));
}
결론
Resume Integrator는 단순한 코드베이스가 아니라 목적을 가지고 구축하기 위한 청사진입니다.
관심사의 분리
- IPFS – 무거운 데이터를 저장합니다.
- Blockchain – 신뢰를 제공합니다.
이점
- 효율적이고 불변이며 확장 가능한 시스템.
- 엄격한 타입 지정과 확인 대기가 사용자에게 신뢰성을 보장합니다.
Resume Protocol은 기반이며, Integrator는 다리 역할을 합니다. 이제 인터페이스를 구축하는 것은 여러분에게 달려 있습니다.
리포지토리
- The Protocol (스마트 계약)
- The Integrator (샘플 클라이언트)
명확함, 목적, 그리고 탁월함으로 신뢰할 수 있는 무언가를 만들어 봅시다.