위임 결제
Pouchi Guard는 위임과 한도를 다룹니다. 위임은 대리인 하나의 서명과 지갑이 강제하는 한도입니다. 이용자는 승인 화면에서 대리인과 한도를 보고 소유자 서명을 한 번 합니다. 그 뒤 대리인은 자기 키로 서명해 한도 안의 호출을 실행하고, 지갑 컨트랙트가 대리인 서명과 한도를 다시 확인합니다.
Pouchi는 위임 실행에 서명하지 않고, 대리인 키를 보관하지 않으며, 대리인이 될 수 없습니다. 실행을 요청만 하는 소프트웨어(에이전트, 예약 작업, 버튼)는 키를 갖지 않고 대리인에게 부탁만 합니다.
실행 방식 두 가지
섹션 제목: “실행 방식 두 가지”| 방식 | 누가 서명 | 언제 쓰나 |
|---|---|---|
| 소유자 실행 (건별 승인) | 이용자 (키 조각 둘) | 매번 이용자가 확인해야 하는 호출. useSendTransaction, 서버의 proposeTransaction |
| 위임 실행 | 대리인 (키 하나) | 같은 종류의 호출을 한도 안에서 반복. 정기 결제, 게임, 에이전트 결제 |
건별 승인은 위임이 아닙니다. 이용자가 그 호출 하나에 소유자 서명을 하는 평소 실행입니다.
공개 에이전트 데모
섹션 제목: “공개 에이전트 데모”AI 에이전트 연결의 URL 하나로 호스팅된 테스트넷 지갑 MCP와 기본 쇼핑 도구를 쓸 수 있습니다. 아래 PC용 지갑 제품을 설치할 필요는 없습니다. PC용 지갑 제품과 새 서버 위임을 만드는 앱 운영자 SDK 함수는 별도로 준비 중입니다. 자체 서비스나 앱 운영자 배포는 business@pouchi.app으로 접근 권한과 설정 절차를 요청하세요.
대리인 종류
섹션 제목: “대리인 종류”위임에는 대리인을 두 가지 방법으로 적습니다. 키 주소를 적거나(key delegate), 앱 운영자 레지스트리 항목을 적습니다(tenant delegate).
| 대리인 | 적는 것 | 키를 가진 쪽 | 쓰는 곳 | SDK |
|---|---|---|---|---|
| 브라우저 세션 키 | 키 주소 | 이용자 (이 브라우저 탭) | 앱 안의 반복 동작 | useSessionApproval (사용 가능) |
| Pouchi 지갑 MCP | 키 주소 | 이용자 (이용자 PC) | 이용자 PC의 AI 에이전트 결제 | 준비 중 |
| 앱 서버 | 앱 운영자 레지스트리 항목 | 앱 운영자 (KMS, HSM) | 정기 결제, 예약 실행, 원격 MCP | executeDelegatedPolicy (사용 가능), 위임 만들기 함수는 준비 중 |
앱 운영자 레지스트리 항목으로 적으면, 앱 운영자가 키를 바꿔도(rotateDeploymentSigner) 이용자가 다시 승인할 필요가 없습니다. 앱 운영자 키를 정지하면 그 앱 운영자가 대리인인 위임만 멈춥니다. 이용자 본인의 실행과 키 주소로 적은 위임에는 영향이 없습니다.
이용자가 서명한 한도는 지갑 컨트랙트에 들어가고, 실행할 때마다 컨트랙트가 확인합니다. 앱 코드나 Pouchi 서버가 한도를 올릴 수 없습니다.
| 한도 | 뜻 |
|---|---|
| 대상과 함수 | 컨트랙트 주소(코드 해시 포함)와 함수 셀렉터. 네이티브 송금은 받는 주소 |
| 금액 | 1회 최대, 합계 최대 |
| 횟수 | 남은 실행 횟수 |
| 기간 | 시작과 끝 시각 |
| 주기 한도 | 고정 길이 창마다 횟수와 금액 (정기 결제) |
| 실행당 최대 가스 | 실행에 허용할 최대 가스. 가스비는 앱이 부담하며 이용자 지갑에서는 차감하지 않음 |
이용자는 언제든 소유자 서명으로 위임을 끌 수 있습니다. 대리인 키가 유출되면 그 위임의 남은 한도까지 쓰일 수 있으니, 작은 한도와 짧은 기간을 권합니다.
앱 안 세션 키
섹션 제목: “앱 안 세션 키”useSessionApproval은 이 브라우저 탭에서 새 키를 만들고, 그 키 주소를 대리인으로 하는 위임을 이용자에게 승인받습니다. 이용자는 사용자 인증(패스키, PIN 등)을 한 번 하고, 그 뒤 세션 안의 호출은 확인 버튼 한 번으로 실행됩니다. 세션 키는 탭 밖으로 나가지 않고 Pouchi도 보지 못합니다.
import { useSendTransaction, useSessionApproval } from "@shard-lab/nc-waas-react";import { encodeFunctionData, parseAbi, parseEther } from "viem";
const GAME = import.meta.env.VITE_GAME_CONTRACT as `0x${string}`;const abi = parseAbi(["function play(uint256 round) payable"]);
export function SessionPlay() { const { session, active, startSession, endSession } = useSessionApproval(); const { sendTransaction } = useSendTransaction();
// One PIN or biometric approval: this function only, at most 0.05 OKRW in 10 calls, for 1 hour. const start = () => startSession({ durationSeconds: 60 * 60, maxTotalValueWei: parseEther("0.05").toString(), maxValuePerCallWei: parseEther("0.005").toString(), maxCalls: 10, contracts: [{ address: GAME, functions: ["play(uint256)"] }] });
// Inside the session this is a one-tap confirmation; outside it, a normal approval. const play = (round: number) => sendTransaction({ to: GAME, value: parseEther("0.005"), data: encodeFunctionData({ abi, functionName: "play", args: [BigInt(round)] }) });
if (!active) return <button onClick={start}>1시간 세션 시작</button>; return ( <div> <p>남은 횟수 {session?.callsRemaining}, 만료 {new Date(session!.expiresAt).toLocaleTimeString()}</p> <button onClick={() => play(1)}>플레이</button> <button onClick={endSession}>세션 끝내기</button> </div> );}import type { ReactNode } from "react";import { NcWaasProvider } from "@shard-lab/nc-waas-react";import { marooTestnet } from "./chain";
export function Providers({ children }: { children: ReactNode }) { return ( <NcWaasProvider apiUrl={import.meta.env.VITE_POUCHI_API_URL} tenantId={import.meta.env.VITE_POUCHI_TENANT_ID} chain={marooTestnet} config={{ loginMethods: ["email"], // The client the tenant registered for this app (web channel). approvals: { session: { clientId: import.meta.env.VITE_POUCHI_SESSION_CLIENT_ID } } }} > {children} </NcWaasProvider> );}| 항목 | 값 |
|---|---|
| 필요한 설정 | config.approvals.session.clientId: 앱 운영자가 이 앱에 등록한 웹 클라이언트 ID |
startSession(input) |
durationSeconds(60초에서 24시간, 기본 1시간), maxTotalValueWei, maxValuePerCallWei, maxCalls(1에서 1,000), contracts({ address, functions }), nativeRecipients |
| 범위 | 함수 최대 16개. 함수는 셀렉터(0x12345678)나 시그니처(play(uint256)) |
| 넣을 수 없는 함수 | transfer, transferFrom, approve, permit, setApprovalForAll처럼 토큰을 옮기거나 허락하는 함수. 세션은 인자를 제한하지 않기 때문에, 이런 호출은 매번 승인합니다 |
| 반환 | { session, active, startSession, endSession }. session에는 expiresAt, callsRemaining, spentWei, valueAllowanceWei, permissions |
세션 안에 드는 호출은 useSendTransaction이 알아서 세션 키로 실행합니다. 세션 밖의 호출(다른 함수, 한도 초과, 만료)은 평소처럼 소유자 승인 화면이 열립니다.
앱 서버 결제 권한
섹션 제목: “앱 서버 결제 권한”정기 결제처럼 이용자가 앱에 없을 때 실행하려면 앱 서버를 대리인으로 둡니다. 서버는 @nc-waas/tenant-server-sdk의 executeDelegatedPolicy로 실행합니다. Pouchi가 정확한 실행 내용을 준비해 digest를 돌려주면 서버의 대리인 키가 서명하고, Pouchi가 제출합니다. 서버가 뚫려도 한도 밖의 요청은 체인에서 실패합니다.
// Tenant server: run one payment inside a delegation whose delegate is this server.// The wallet contract checks this server's signature and every limit the user signed.import { executeDelegatedPolicy, type PolicyDelegateSigner } from "@nc-waas/tenant-server-sdk";import { encodeFunctionData, parseAbi, parseUnits, type Address, type Hex } from "viem";
const erc20 = parseAbi(["function transfer(address to, uint256 amount) returns (bool)"]);
// Production: a KMS or HSM key that signs digests. The private key never leaves it.declare function kmsSignDigest(keyId: string, digest: Hex): Promise<Hex>;const signer: PolicyDelegateSigner = { address: process.env.DELEGATE_ADDRESS as Address, signDigest: (digest) => kmsSignDigest(process.env.DELEGATE_KMS_KEY_ID!, digest)};
export async function chargeMonthly(input: { connectionToken: string; policyId: string; invoiceId: string; merchant: Address }) { const result = await executeDelegatedPolicy({ apiUrl: process.env.POUCHI_API_URL!, connectionToken: input.connectionToken, // app connection token with scope policy:execute policyId: input.policyId, requestId: `invoice-${input.invoiceId}`, // stable per payment: a retry never pays twice calls: [{ target: process.env.TOKEN_ADDRESS as Address, data: encodeFunctionData({ abi: erc20, functionName: "transfer", args: [input.merchant, parseUnits("9900", 18)] }) }], signer }); if (!result.ok) throw new Error(`charge ${result.status}: ${result.code ?? ""} ${result.failureReason ?? ""}`); return result.transactionHash;}| 입력 | 뜻 |
|---|---|
apiUrl |
Pouchi API 주소 |
connectionToken |
이용자 지갑에 대한 앱 연결 토큰 (scope policy:execute) |
policyId |
이용자가 승인한 위임 ID |
requestId |
결제마다 고정하는 ID. 같은 ID로 다시 보내도 두 번 결제되지 않음 |
calls |
{ target, value?, data }[]. 위임의 대상과 함수 안이어야 함 |
signer |
PolicyDelegateSigner ({ address, signDigest(digest) }). 운영에서는 KMS나 HSM |
responseMode |
"completed"(체인 결과까지 기다림) 또는 "submitted" |
결과는 { ok, status, operationId?, transactionHash?, userOperationHash?, failureReason?, code? }입니다.
이용자 PC의 에이전트
섹션 제목: “이용자 PC의 에이전트”이용자가 자기 PC에 Pouchi 지갑 MCP를 설치하면, 대리인 키가 그 PC에서 만들어지고 밖으로 나가지 않습니다. 승인 화면에는 “이 PC의 Pouchi 지갑 MCP”로 표시되고, 대리인은 이용자 자신입니다. 도구는 wallet_status, pay_intent, request_budget, payment_status, revoke_mandate입니다. 제품 배포는 준비 중입니다.
연동 코드를 쓰는 개발자용 MCP(문서와 예시)는 이것과 다른 도구입니다. AI 개발 도구 연동