콘텐츠로 건너뛰기

Delegation Toolkit을 사용하여 dapp에 소셜 초대 기능을 구축하는 방법

Delegation Toolkit을 사용하여 dapp에 소셜 초대 기능 구축하기

5분
Delegation Toolkit을 사용하여 dapp에 소셜 초대 기능을 구축하는 방법

이전 블로그 게시물인 Delegation Toolkit이란 무엇이며 무엇을 만들 수 있나요?에서 MetaMask의 Delegation Toolkit이 개발자들에게 보다 유연하고 사용자 친화적인 dapp을 만들 수 있는 역량을 어떻게 부여하는지 살펴보았습니다. 특히 주목할 만한 활용 사례 중 하나는 사용자가 최소한의 번거로움으로 친구를 온보딩할 수 있는 소셜 초대 기능을 구축하는 것입니다.

이 튜토리얼에서는 기존 사용자가 다른 사용자에게 제한된 권한을 위임하여 새로운 사용자가 지갑 설정 및 자금 충전이라는 일반적인 장벽 없이 dapp을 탐색할 수 있도록 하는 흐름을 구현하는 방법을 안내합니다.

이런 상황을 상상해 보세요. Alice가 자신이 사용 중인 dapp을 Bob에게 소개하고 싶습니다. Alice는 Bob에게 초대장을 보내어 그가 일정 시간 내에 자신의 지갑에서 소량의 ETH(예: 0.001 ETH)를 클레임할 수 있도록 합니다. Bob은 지갑 확장 프로그램을 설치하거나 가스비를 직접 지불할 필요 없이 즉시 dapp을 사용할 수 있습니다. 한편 Alice는 개인 키를 노출하지 않고 자신의 지갑을 완전히 통제할 수 있습니다.

이것이 바로 Delegation Toolkit이 권한 위임과 안전한 오프체인 서명을 활용하여 가능하게 하는 간소화된 온보딩 경험입니다. 이제 구축할 내용의 구조를 간략히 살펴보겠습니다.

초대자(Inviter)

초대자는 다른 사람의 온보딩을 돕기 위해 자신의 지갑에 대한 제한된 접근 권한을 공유하려는 사용자입니다. 이 튜토리얼에서 초대자는 EIP-4337 기반의 스마트 컨트랙트 지갑인 MetaMask Delegator 계정을 사용합니다. 새로운 delegator 계정을 생성하거나 이미 설정한 기존 계정을 사용할 수 있습니다. 배포 및 인프라 처리를 위해 Pimlico의 EIP-4337 서비스를 사용할 것입니다.

초대장(Invitation)

이 맥락에서 초대장은 단순히 delegation, 즉 delegator 계정이 생성하여 오프체인에 저장하는 서명된 객체입니다. delegator 계정이 배포되고 ETH로 자금이 충전되면, 다른 사용자가 특정 기간 내에 일정량의 토큰을 클레임할 수 있는 delegation을 생성할 수 있습니다. 이 설정은 초대받은 사람이 지갑에 자금을 충전하거나 가스비를 선불로 지불할 필요를 없애 온보딩을 훨씬 원활하게 만듭니다.

간단하게 유지하기 위해 오픈 delegation을 사용합니다. 이는 서명된 delegation에 접근할 수 있는 누구나 이를 사용할 수 있음을 의미합니다. 특정 수신자에게 바인딩되지 않으므로 초대자의 개인 키를 관리하거나 노출할 필요가 없습니다.

초대받은 사람(Invitee)

초대받은 사람은 온보딩되는 사람입니다. 구현 방식에 따라 외부 소유 계정(EOA) 또는 스마트 컨트랙트 계정(SCA)을 사용할 수 있습니다. 이 튜토리얼에서는 초대받은 사람이 클레임 링크를 클릭할 때 자동으로 EOA를 생성합니다. 프로덕션 dapp에서는 초대받은 사람이 이미 연결된 지갑이 있는지 감지하여 해당 지갑을 사용하는 것이 이상적이지만, 그 로직 구현은 이 가이드의 범위를 벗어납니다.

조건(Conditions)

각 초대장에는 사용 방법과 시기를 제어하는 조건을 포함할 수 있습니다. caveats라고도 하는 이 조건들은 자금 한도, 사용 기한, 또는 delegation을 사용할 수 있는 횟수 제한을 지정할 수 있습니다. 이 경우, 기본적인 안전을 보장하기 위해 delegation을 최대 토큰 금액(X)과 시간 창(Y)으로 한 번만 사용 가능하도록 설정합니다.

초대 수락(Accepting the Invitation)

초대받은 사람이 클레임 링크를 클릭하고 초대를 수락하면 delegation을 사용(redeem)하게 됩니다. 이 과정은 위임된 자금을 전송하기 위해 블록체인에서 사용자 작업(user operation)을 트리거합니다. 이 작업을 전송하고 흐름을 완료하기 위해 다시 Pimlico의 인프라를 활용합니다.

이제 흐름을 이해했으니 구현을 시작해 보겠습니다.

사전 요구 사항

구축을 시작하기 전에 다음이 설정되어 있는지 확인하세요:

  • Node.js (버전 18 이상)

  • Sepolia 테스트넷을 위한 Infura RPC 엔드포인트

  • Paymaster, Bundler, User Operations 등 핵심 개념을 포함한 EIP-4337 계정 추상화에 대한 기본적인 이해

  • Pimlico Bundler 및 Paymaster URL

의존성 설치

먼저 delegation toolkit을 설치합니다:

npm install @metamask/delegation-toolkit

다음으로 모든 이더리움 상호작용을 위해 Viem을 설치합니다:

npm install viem

초대자 계정 생성

초대자 계정은 새로운 사용자의 온보딩을 돕기 위해 제한된 권한을 공유하는 지갑입니다. dapp과의 초기 상호작용 중 초대받은 사람의 가스비를 충당할 수 있을 만큼 충분한 암호화폐를 보유해야 합니다. 이 튜토리얼에서는 이 역할을 수행하기 위해 Gator 계정이라고도 불리는 스마트 컨트랙트 계정을 사용합니다.

다만, Pectra 업그레이드 덕분에 EIP-7702를 지원하는 일반 EOA도 이제 권한을 위임할 수 있습니다. 이를 통해 기존 지갑이 스마트 컨트랙트 계정으로 전환하지 않고도 권한 공유에 참여할 수 있는 가능성이 열립니다. 이 접근 방식에 대해서는 별도의 게시물에서 더 자세히 다룰 예정입니다.

시작하려면 프로젝트 루트에 index.ts 파일을 생성하고 다음 코드 스니펫을 추가합니다:

import { http, createPublicClient } from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { sepolia as chain } from "viem/chains";
import {
  Implementation,
  toMetaMaskSmartAccount,
} from "@metamask/delegator-core-viem";

const RPCEndpoint = "<INFURA RPC ENDPOINT HERE>";
const transport = http(RPCEndpoint);
const publicClient = createPublicClient({ transport, chain });

const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);

const deploySalt = "0x";

const account = await toMetaMaskSmartAccount({
  client: publicClient,
  implementation: Implementation.Hybrid,
  deployParams: [owner.address, [], [], []],
  deploySalt,
  signatory: { account: owner },
});
복사

delegator 계정을 생성할 때 두 가지 주요 옵션이 있습니다:

  • 하이브리드 계정(Hybrid account): 이 유형은 EOA 또는 P256 호환 서명자가 될 수 있는 단 하나의 서명자만 필요합니다. implementation: Implementation.Hybrid을 사용하여 지정합니다.

  • 멀티시그 계정(Multisig account): 이 옵션은 delegator 계정을 대신하여 서명할 여러 EOA가 필요합니다. implementation: Implementation.MultiSig을 사용하여 지정합니다.

이 튜토리얼에서는 하이브리드 계정을 사용하고 서명자를 일반 EOA로 설정합니다. 위의 예시에서 개인 키를 생성하고 있지만, 이는 기존 지갑의 개인 키를 프로그래밍 방식으로 사용하도록 조정할 수 있습니다.

delegator 계정을 생성했으니 중요한 세부 사항을 기억해 두세요: toMetaMaskSmartAccount를 사용하여 생성된 계정은 *반사실적(counterfactual)*입니다. 이는 계정이 배포될 때까지 온체인에 존재하지 않음을 의미합니다. account.address를 사용하여 계정 주소를 검색할 수 있지만, 블록 탐색기에서 검색하면 아직 찾을 수 없습니다.

그러나 해당 주소로 미리 토큰을 전송할 수 있습니다. 계정이 배포되면 해당 토큰에 접근할 수 있으며, 해당 주소에 연결된 보류 중인 작업을 즉시 실행할 수 있습니다.

초대자 Delegator 계정 배포

진행하려면 반사실적 계정을 배포하여 블록체인과 상호작용할 수 있도록 해야 합니다. 대부분의 dapp에서 이 단계는 일반적으로 사용자가 로그인하거나 가입할 때와 같이 온보딩 중에 트리거됩니다.

배포를 처리하기 위해 Pimlico의 인프라를 사용합니다. Pimlico 대시보드로 이동하여 API 키를 생성하고 제공된 URL을 복사합니다. 이 URL은 번들러(사용자 작업 중계)와 paymaster(가스비 후원) 역할을 모두 합니다.

반사실적 계정을 배포하는 전용 함수가 없으므로, 번들러에 더미 사용자 작업을 제출하여 배포를 트리거합니다. 이 작업은 성공하지 않지만, Pimlico는 실행을 시도하기 전에 계정을 배포하며, 이것이 우리에게 필요한 전부입니다.

Pimlico 번들러 및 paymaster 설정

Pimlico의 번들러, paymaster, 사용자 작업 도구와 상호작용하기 위해 @pimlico/permissionless 패키지를 설치합니다:

npm install permissionless

다음으로 Pimlico 번들러와 paymaster를 구성하고 Gator 계정을 배포합니다:

… 
import {
  createBundlerClient,
  createPaymasterClient,
} from "viem/account-abstraction";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { zeroAddress } from "viem";

const pimlicoURL = process.env.PIMLICO_URL;

  const paymasterClient = createPaymasterClient({
    transport: http(pimlicoURL),
  });

  const bundlerClient = createBundlerClient({
    transport: http(pimlicoURL),
    paymaster: paymasterClient,
  });

  const pimlicoClient = createPimlicoClient({
    transport: http(pimlicoURL),
  });;

// deploy the gator account
const { fast: gasPrice } = await pimlicoClient.getUserOperationGasPrice();
const isDeployed = await account.isDeployed();
if (!isDeployed) {
  const hash = await bundlerClient.sendUserOperation({
    account,
    calls: [{ to: zeroAddress }],
    ...gasPrice,
  });

  const { receipt } = await bundlerClient.waitForUserOperationReceipt({ hash });
}
복사

위 스니펫에서 먼저 Pimlico paymaster와 번들러 클라이언트를 초기화한 다음 Pimlico 클라이언트 자체를 초기화합니다. 그런 다음 Gator 계정이 이미 배포되었는지 확인합니다. 배포되지 않은 경우, 아직 배포되지 않은 계정을 발신자로 사용하여 더미 사용자 작업을 전송합니다.

Pimlico 번들러는 사용자 작업이 번들링되어 멤풀에 제출되면 트랜잭션 해시를 반환합니다. 해당 해시를 사용하여 트랜잭션이 채굴되고 온체인에서 확인될 때까지 기다립니다.

이 프로세스가 내부적으로 어떻게 작동하는지에 대한 자세한 내용은 Pimlico 문서를 참조하세요.

마지막 단계는 새로 배포된 delegator 계정에 테스트 ETH를 충전하는 것입니다. MetaMask Sepolia 파우셋을 방문하여 이를 수행할 수 있습니다.

초대장 생성

초대자를 위한 자금이 충전된 delegator 계정이 설정되었으니, 다음 단계는 오픈 delegation을 생성하고 서명하는 것입니다. 이 delegation은 초대받은 사람이 초대자의 계정에서 특정 양의 테스트 ETH를 클레임할 수 있도록 합니다.

Delegation은 다음 구조를 가진 DelegationStruct 타입을 사용하여 정의됩니다:

export type DelegationStruct = {
  delegate: Hex;             // The address receiving the delegated permission
  delegator: Hex;            // The address granting the permission
  authority: Hex;            // The parent delegation hash, or ROOT_AUTHORITY if it's the root
  caveats: CaveatStruct[];   // Optional rules that limit what the delegate can do
  salt: bigint;              // A random value to avoid hash collisions
  signature: Hex;            // The delegator's signature
};
복사

이 경우, delegation 체인의 첫 번째 링크인 루트 delegation을 생성합니다. 루트 delegation 위에 구축하여 재위임 체인을 만들 수 있지만, 이 튜토리얼에서는 루트 delegation 자체에만 집중합니다.

루트 delegation을 생성하는 두 가지 방법이 있습니다:

  • 이 튜토리얼처럼 delegate의 주소를 미리 알 수 없는 경우, 어떤 계정이든 사용할 수 있는 오픈 delegation을 생성할 수 있습니다.

  • delegate의 주소를 알고 있는 경우, delegate 필드를 해당 주소로 명시적으로 설정하여 클로즈드 delegation을 생성할 수 있습니다.

유연성과 단순성을 위해 오픈 delegation 방식을 선택합니다:

...
import {
...
  createOpenDelegation,
} from "@metamask/delegator-core-viem";

const caveatBuilder = createCaveatBuilder(account.environment);
const caveats = caveatBuilder
  .addCaveat("limitedCalls", 1)
  .addCaveat("nativeTokenTransferAmount", BigInt(1))
  .addCaveat("timestamp", 0, Math.floor(Date.now() / 1000) + 86_400);

const openDelegation = createOpenDelegation({
  from: account.address,
  caveats,
});
복사

이 단계에서 delegation 사용 방법을 제어하기 위해 세 가지 caveat을 추가했습니다:

  • 실행 제한: limitedCalls caveat을 사용하여 delegate가 초대자를 대신하여 작업을 수행할 수 있는 횟수를 제한합니다.

  • 토큰 전송 한도: nativeTokenTransferAmount caveat은 초대자의 계정에서 전송할 수 있는 네이티브 토큰의 총 금액을 제한합니다.

  • 만료: 특정 시간 이후 delegation이 무효화되도록 타임스탬프 기반 caveat을 추가합니다. 이 경우 초대장은 24시간 후에 만료됩니다.

포함할 수 있는 다른 많은 caveat이 있지만, 이것들로 우리의 사용 사례에 충분합니다. 문서에서 지원되는 caveat의 전체 목록을 찾을 수 있으며, 필요한 경우 커스텀 caveat을 정의할 수도 있습니다.

delegation이 정의되면 마지막 단계는 서명하고 저장하는 것입니다. delegation이 유효하려면 delegator가 서명해야 합니다. MetaMask delegator 계정은 이 목적을 위해 사용할 signDelegation 메서드를 노출합니다.

저장 방법은 원하는 방법을 선택할 수 있습니다. 이 튜토리얼에서는 간단하게 localStorage를 사용합니다. JavaScript의 내장 JSON.stringify는 BigInt 값을 지원하지 않으므로 superjson 라이브러리의 stringify를 사용하여 delegation을 직렬화합니다.

npm install superjson
...
import { stringify } from "superjson";


const signature = await account.signDelegation({
  delegation: openDelegation,
});

localStorage.setItem(
  "DELEGATION",
  stringify({ delegation: openDelegation, signature })
);
복사

초대장 사용(Redeem)

이제 초대받은 사람의 관점으로 전환해 보겠습니다. 즉, delegation을 사용하고 초대자의 계정에서 자신의 계정으로 토큰을 전송하는 등의 작업을 수행할 사용자입니다.

1단계: 저장소에서 delegation 가져오기

서명된 delegation을 로컬에 저장했으므로, 초대받은 사람은 localStorage에서 직접 가져올 수 있습니다:

import { parse } from "superjson";

const storedData = localStorage.getItem("DELEGATION");

if (!storedData) {
  throw new Error("Delegation not found in storage");
}

const { delegation, signature } = parse(storedData);
복사

2단계: 실행 생성

실행(execution)은 delegate가 delegator의 계정에서 수행할 수 있는 특정 작업을 나타냅니다. delegation에 정의된 모든 caveat을 준수해야 합니다.

실행 객체는 일반적으로 다음과 같습니다:

{
  target,   // the address being called as a hex string
  value,    // the value of the call as a bigint
  callData  // the calldata as a hex string
}
복사

간단하게 하기 위해 SDK의 헬퍼 메서드 createExecution을 사용할 수 있습니다:

import { createExecution } from "@codefi/delegator-core-viem";

const execution = createExecution(
       target,   // the address being called as a hex string
  	 value,    // the value of the call as a bigint
  	  callData  // the calldata as a hex string
      );
복사

이 경우, 스마트 컨트랙트를 호출하는 것이 아니라 단순히 delegator에서 delegate로 ETH를 전송하는 것이므로 실행은 다음과 같습니다:

const execution = createExecution(
        delegateAccount.address,
        amount
      );
복사

3단계: UserOperation으로 실행 제출

계정 추상화 개념이 처음이라면 이 부분이 다소 복잡할 수 있으니 단계별로 살펴보겠습니다.

높은 수준에서 우리의 목표는 delegation을 사용하는 데 필요한 모든 트랜잭션 데이터를 래핑하는 UserOperation을 제출하는 것입니다. 추상적으로 보일 수 있지만, 이는 궁극적으로 온체인에서 실행되는 표준 이더리움 트랜잭션이 됩니다.

이 트랜잭션은 Delegator 스마트 컨트랙트 계정에서 redeemDelegations라는 함수를 호출합니다. 이 함수는 다음을 담당합니다:

  1. delegation 검증 및 caveat 유효성 확인

  2. 실행 처리 방법 결정(redeem 모드). 자세한 내용은 여기 를 참조하세요

  3. 원하는 작업 실행(이 경우 토큰 전송)

간단히 말해, redeemDelegations는 delegation이 여전히 유효한지 확인하고, 모든 것이 확인되면 의도한 작업을 수행합니다.

이 트랜잭션을 전송하기 위해 번들러 클라이언트가 제공하는 sendUserOperation 메서드를 사용합니다. 이는 UserOperation 구성 및 서명의 복잡성을 추상화합니다:

import { encodeFunctionData } from "viem";
import {
  encodeExecutionCalldatas,
  encodePermissionContexts,
  SINGLE_DEFAULT_MODE,
} from "@codefi/delegator-core-viem";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { createBundlerClient } from "viem/account-abstraction";

 const bundlerClient = createBundlerClient({
        transport: http(process.env.NEXT_PUBLIC_BUNDLER_URL),
        chain: lineaSepolia,
      });

      const pimlicoClient = createPimlicoClient({
        transport: http(process.env.NEXT_PUBLIC_BUNDLER_URL),
        chain: lineaSepolia,
      });

      const { fast } = await pimlicoClient.getUserOperationGasPrice();


     
  const redeemData = encodeFunctionData({
          abi: delegateAccount.abi,
          functionName: "redeemDelegations",
          args: [
            encodePermissionContexts([delegation]),
            [SINGLE_DEFAULT_MODE],
            encodeExecutionCalldatas([[execution]]),
          ],
        });

      const hash = await bundlerClient.sendUserOperation({
        account: delegateAccount,
        calls: [
          {
            to: delegateAccount.address,
            value: 0n,
            data: redeemData,
          },
        ],
        paymaster: true,
        ...fast,
      });


  await bundlerClient.waitForUserOperationReceipt({
        hash,
      });
복사

AI가 번역했습니다. 오류가 있을 수 있습니다. 항상 정보를 확인하시기 바랍니다.

번역 평가하기

이 글의 작성자:

  • Kingsley Okonkwo

    공인 이더리움 블록체인 개발자인 Kingsley는 Consensys에서 테크니컬 라이터로 활동하며, 특히 개발자 도구에 중점을 두고 있습니다. Consensys에 합류하기 전, Kingsley는 Gigster와 Braintrust를 포함한 여러 플랫폼에서 프리랜서 백엔드 개발자로 활동했습니다. 5년간의 소프트웨어 개발 경험을 바탕으로, JavaScript(Node.js 및 React), GoLang, SQL, Docker 등의 도구에 능숙합니다. Kingsley는 최신 web3 기술을 활용해 단계별 튜토리얼과 사용 방법 가이드를 제작하는 데 탁월하며, 동료 개발자들이 더 나은 dapp을 구축할 수 있도록 돕고 있습니다. 현재 두바이에 거주하고 있는 Kingsley는 여가 시간에 농구와 축구를 즐깁니다.

    모든 글 읽기