TON Connect for React
React 앱을 위한 권장 SDK는 UI React SDK입니다. TON Connect와 상호작용하기 위한 고수준의 React 컴포넌트를 제공합니다.
구현
설치
DApp에 TON Connect를 통합하려면 @tonconnect/ui-react 패키지를 설치해야 합니다. npm이나 yarn을 사용할 수 있습니다:
- npm
- Yarn
- pnpm
npm i @tonconnect/ui-react
yarn add @tonconnect/ui-react
pnpm add @tonconnect/ui-react
TON Connect 초기화
패키지 설치 후, 애플리케이션을 위한 manifest.json 파일을 생성해야 합니다. manifest.json 파일 생성 방법은 여기에서 확인할 수 있습니다.
manifest 파일을 생성한 후, TonConnectUIProvider를 Mini App의 루트에 임포트하고 manifest URL을 전달하세요:
import { TonConnectUIProvider } from '@tonconnect/ui-react';
export function App() {
    return (
        <TonConnectUIProvider manifestUrl="https://<YOUR_APP_URL>/tonconnect-manifest.json">
            { /* Your app */ }
        </TonConnectUIProvider>
    );
}
지갑 연결
TonConnectButton을 추가하세요. TonConnect 버튼은 연결을 초기화하기 위한 범용 UI 컴포넌트입니다. 지갑이 연결되면 지갑 메뉴로 변환됩니다. 앱의 우측 상단에 배치하는 것을 권장합니다.
export const Header = () => {
  return (
    <header>
      <span>My App with React UI</span>
      <TonConnectButton />
    </header>
  );
};
버튼에 className과 style props도 추가할 수 있습니다. TonConnectButton에는 하위 요소를 전달할 수 없습니다:
<TonConnectButton className="my-button-class" style={{ float: "right" }}/>
또한 useTonConnectUI 훅과 openModal 메서드를 사용하여 수동으로 연결을 시작할 수 있습니다.
export const Header = () => {
  const [tonConnectUI, setOptions] = useTonConnectUI();
  return (
    <header>
      <span>My App with React UI</span>
      <button onClick={() => tonConnectUI.openModal()}>
        Connect Wallet
      </button>
    </header>
  );
};
특정 지갑과 연결
특정 지갑에 대한 모달 창을 열려면 openSingleWalletModal() 메서드를 사용하세요. 이 메서드는 지갑의 app_name을 파라미터로 받아(wallets-list.json 파일 참조) 해당 지갑 모달을 엽니다. 모달 창이 성공적으로 열리면 프로미스가 해결됩니다.
<button onClick={() => tonConnectUI.openSingleWalletModal('tonwallet')}>
  Connect Wallet
</button>
리다이렉트
지갑 연결 후 사용자를 특정 페이지로 리다이렉트하려면 useTonConnectUI 훅을 사용하고 return strategy를 커스터마이징할 수 있습니다.
Telegram Mini Apps
지갑 연결 후 사용자를 Telegram Mini App으로 리다이렉트하려면 TonConnectUIProvider 엘리먼트를 커스터마이징할 수 있습니다:
      <TonConnectUIProvider
            // ... other parameters
          actionsConfiguration={{
              twaReturnUrl: 'https://t.me/<YOUR_APP_NAME>'
          }}
      >
      </TonConnectUIProvider>
UI 커스터마이징
모달의 UI를 커스터마이징하려면 useTonConnectUI 훅과 setOptions 함수를 사용할 수 있습니다. useTonConnectUI 훅에 대해 자세히 알아보려면 Hooks 섹션을 참조하세요.
Hooks
React 앱에서 저수준 TON Connect UI SDK 기능을 사용하려면 @tonconnect/ui-react 패키지의 훅을 사용할 수 있습니다.
useTonAddress
현재 사용자의 ton 지갑 주소를 가져오는데 사용합니다. 주소 형식을 선택하려면 boolean 파라미터 isUserFriendly(기본값 true)를 전달하세요. 지갑이 연결되어 있지 않으면 훅은 빈 문자열을 반환합니다.
import { useTonAddress } from '@tonconnect/ui-react';
export const Address = () => {
  const userFriendlyAddress = useTonAddress();
  const rawAddress = useTonAddress(false);
  return (
    userFriendlyAddress && (
      <div>
        <span>User-friendly address: {userFriendlyAddress}</span>
        <span>Raw address: {rawAddress}</span>
      </div>
    )
  );
};
useTonConnectModal
모달 창을 열고 닫는 함수에 접근하는데 사용하는 훅입니다. 현재 모달 상태와 모달을 열고 닫는 메서드를 포함하는 객체를 반환합니다.
import { useTonConnectModal } from '@tonconnect/ui-react';
export const ModalControl = () => {
    const { state, open, close } = useTonConnectModal();
    return (
      <div>
          <div>Modal state: {state?.status}</div>
          <button onClick={open}>Open modal</button>
          <button onClick={close}>Close modal</button>
      </div>
    );
};
useTonWallet
현재 사용자의 TON 지갑을 가져오는데 사용하는 훅입니다.
지갑이 연결되어 있지 않으면 null을 반환합니다. wallet 객체는 사용자의 주소, provider, TON proof 및 기타 속성과 같은 일반적인 데이터를 제공합니다(Wallet 인터페이스 참조).
또한 연결된 지갑의 이름, 이미지 및 기타 속성과 같은 더 구체적인 세부 정보에도 접근할 수 있습니다(WalletInfo 인터페이스 참조).
import { useTonWallet } from '@tonconnect/ui-react';
export const Wallet = () => {
  const wallet = useTonWallet();
  return (
    wallet && (
      <div>
        <span>Connected wallet address: {wallet.account.address}</span>
        <span>Device: {wallet.device.appName}</span>
        <span>Connected via: {wallet.provider}</span>
        {wallet.connectItems?.tonProof?.proof && <span>Ton proof: {wallet.connectItems.tonProof.proof}</span>}
        <div>Connected wallet info:</div>
        <div>
          {wallet.name} <img src={wallet.imageUrl} />
        </div>
      </div>
    )
  );
};
useTonConnectUI
TonConnectUI 인스턴스와 UI 옵션 업데이트 함수에 접근하는데 사용합니다.
TonConnectUI 인스턴스 메서드에 대해 자세히 알아보기
import { Locales, useTonConnectUI } from '@tonconnect/ui-react';
export const Settings = () => {
  const [tonConnectUI, setOptions] = useTonConnectUI();
  const onLanguageChange = (language: Locales) => {
    setOptions({ language });
  };
  return (
    <div>
      <label>language</label>
      <select onChange={(e) => onLanguageChange(e.target.value as Locales)}>
        <option value="en">en</option>
        <option value="ru">ru</option>
      </select>
    </div>
  );
};
useIsConnectionRestored
연결 복원 프로세스의 현재 상태를 나타냅니다. 연결 복원 프로세스가 완료되었는지 감지하는데 사용할 수 있습니다.
import { useIsConnectionRestored } from '@tonconnect/ui-react';
export const EntrypointPage = () => {
  const connectionRestored = useIsConnectionRestored();
  if (!connectionRestored) {
    return <Loader>Please wait...</Loader>;
  }
  return <MainPage />;
};
사용법
React UI SDK를 실제로 어떻게 사용하는지 살펴보겠습니다.
트랜잭션 전송
특정 주소로 TON 코인(nanotons 단위)을 보내기:
import { useTonConnectUI } from '@tonconnect/ui-react';
const transaction: SendTransactionRequest = {
  validUntil: Date.now() + 5 * 60 * 1000, // 5 minutes
  messages: [
    {
      address:
        "0QD-SuoCHsCL2pIZfE8IAKsjc0aDpDUQAoo-ALHl2mje04A-", // message destination in user-friendly format
      amount: "20000000", // Toncoin in nanotons
    },
  ],
};
export const Settings = () => {
  const [tonConnectUI, setOptions] = useTonConnectUI();
  return (
    <div>
      <button onClick={() => tonConnectUI.sendTransaction(transaction)}>
        Send transaction
      </button>
    </div>
  );
};
- 더 많은 예제는 여기에서 확인하세요: 메시지 준비하기
해시를 통한 트랜잭션 상태 이해
원리는 Payment Processing(tonweb 사용)에 있습니다. 자세히 보기
백엔드에서 선택적 체크(ton_proof)
메시지 서명과 검증 방법 이해하기: 서명과 검증
사용자가 선언된 주소를 실제로 소유하고 있는지 확인하기 위해 ton_proof를 사용할 수 있습니다.
연결 요청 파라미터를 설정하려면 tonConnectUI.setConnectRequestParameters 함수를 사용하세요. 다음과 같은 용도로 사용할 수 있습니다:
- 로딩 상태: 백엔드로부터 응답을 기다리는 동안 로딩 상태를 표시합니다.
- 준비 상태와 tonProof: 상태를 'ready'로 설정하고 tonProof 값을 포함시킵니다.
- 오류가 발생하면 로더를 제거하고 추가 파라미터 없이 연결 요청을 생성합니다.
const [tonConnectUI] = useTonConnectUI();
// Set loading state
tonConnectUI.setConnectRequestParameters({ state: "loading" });
// Fetch tonProofPayload from backend
const tonProofPayload: string | null =
  await fetchTonProofPayloadFromBackend();
if (tonProofPayload) {
  // Set ready state with tonProof
  tonConnectUI.setConnectRequestParameters({
    state: "ready",
    value: { tonProof: tonProofPayload },
  });
} else {
  // Remove loader
  tonConnectUI.setConnectRequestParameters(null);
}
ton_proof 결과 처리
지갑이 연결되었을 때 wallet 객체에서 ton_proof 결과를 찾을 수 있습니다:
useEffect(() => {
    tonConnectUI.onStatusChange((wallet) => {
      if (
        wallet.connectItems?.tonProof &&
        "proof" in wallet.connectItems.tonProof
      ) {
        checkProofInYourBackend(
          wallet.connectItems.tonProof.proof,
          wallet.account.address
        );
      }
    });
  }, [tonConnectUI]);
ton_proof의 구조
type TonProofItemReplySuccess = {
  name: "ton_proof";
  proof: {
    timestamp: string; // Unix epoch time (seconds)
    domain: {
      lengthBytes: number; // Domain length
      value: string;  // Domain name
    };
    signature: string; // Base64-encoded signature
    payload: string; // Payload from the request
  }
}
인증 예제는 이 페이지에서 확인할 수 있습니다.
지갑 연결 해제
지갑 연결을 해제하려면:
const [tonConnectUI] = useTonConnectUI();
await tonConnectUI.disconnect();
컨트랙트 배포
TonConnect를 사용한 컨트랙트 배포는 매우 간단합니다. 컨트랙트 코드와 state init을 얻어 cell로 저장하고 stateInit 필드와 함께 트랜잭션을 전송하면 됩니다.
CONTRACT_CODE와 CONTRACT_INIT_DATA는 래퍼에서 찾을 수 있습니다.
import { beginCell, Cell, contractAddress, StateInit, storeStateInit } from '@ton/core';
const [tonConnectUI] = useTonConnectUI();
const init = {
    code: Cell.fromBase64('<CONTRACT_CODE>'),
    data: Cell.fromBase64('<CONTRACT_INIT_DATA>')
} satisfies StateInit;
const stateInit = beginCell()
    .store(storeStateInit(init))
    .endCell();
const address = contractAddress(0, init);
await tonConnectUI.sendTransaction({
    validUntil: Date.now() + 5 * 60 * 1000, // 5 minutes
    messages: [
        {
            address: address.toRawString(),
            amount: '5000000',
            stateInit: stateInit.toBoc().toString('base64')
        }
    ]
});
래퍼
래퍼는 기본 세부 사항에 신경 쓰지 않고도 컨트랙트와 상호작용할 수 있게 해주는 클래스입니다.
- FunC로 컨트랙트를 개발할 때는 래퍼를 직접 작성해야 합니다.
- Tact 언어를 사용할 때는 래퍼가 자동으로 생성됩니다.
컨트랙트 개발 및 배포 방법은 blueprint 문서를 확인하세요
기본 Blueprint Counter 래퍼 예제와 사용 방법을 살펴보겠습니다:
Details
래퍼 사용법
Counter 래퍼 클래스:import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';
export type CounterConfig = {
    id: number;
    counter: number;
};
export function counterConfigToCell(config: CounterConfig): Cell {
    return beginCell().storeUint(config.id, 32).storeUint(config.counter, 32).endCell();
}
export const Opcodes = {
    increase: 0x7e8764ef,
};
export class Counter implements Contract {
    constructor(
        readonly address: Address,
        readonly init?: { code: Cell; data: Cell },
    ) {}
    static createFromAddress(address: Address) {
        return new Counter(address);
    }
    static createFromConfig(config: CounterConfig, code: Cell, workchain = 0) {
        const data = counterConfigToCell(config);
        const init = { code, data };
        return new Counter(contractAddress(workchain, init), init);
    }
    async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
        await provider.internal(via, {
            value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
            body: beginCell().endCell(),
        });
    }
    async sendIncrease(
        provider: ContractProvider,
        via: Sender,
        opts: {
            increaseBy: number;
            value: bigint;
            queryID?: number;
        },
    ) {
        await provider.internal(via, {
            value: opts.value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
            body: beginCell()
                .storeUint(Opcodes.increase, 32)
                .storeUint(opts.queryID ?? 0, 64)
                .storeUint(opts.increaseBy, 32)
                .endCell(),
        });
    }
    async getCounter(provider: ContractProvider) {
        const result = await provider.get('get_counter', []);
        return result.stack.readNumber();
    }
    async getID(provider: ContractProvider) {
        const result = await provider.get('get_id', []);
        return result.stack.readNumber();
    }
}
그런 다음 React 컴포넌트에서 이 클래스를 사용할 수 있습니다:
import "buffer";
import {
  TonConnectUI,
  useTonConnectUI,
  useTonWallet,
} from "@tonconnect/ui-react";
import {
  Address,
  beginCell,
  Sender,
  SenderArguments,
  storeStateInit,
  toNano,
  TonClient,
} from "@ton/ton";
class TonConnectProvider implements Sender {
  /**
   * The TonConnect UI instance.
   * @private
   */
  private readonly provider: TonConnectUI;
  /**
   * The address of the current account.
   */
  public get address(): Address | undefined {
    const address = this.provider.account?.address;
    return address ? Address.parse(address) : undefined;
  }
  /**
   * Creates a new TonConnectProvider.
   * @param provider
   */
  public constructor(provider: TonConnectUI) {
    this.provider = provider;
  }
  /**
   * Sends a message using the TonConnect UI.
   * @param args
   */
  public async send(args: SenderArguments): Promise<void> {
    // The transaction is valid for 10 minutes.
    const validUntil = Math.floor(Date.now() / 1000) + 600;
    // The address of the recipient, should be in bounceable format for all smart contracts.
    const address = args.to.toString({ urlSafe: true, bounceable: true });
    // The address of the sender, if available.
    const from = this.address?.toRawString();
    // The amount to send in nano tokens.
    const amount = args.value.toString();
    // The state init cell for the contract.
    let stateInit: string | undefined;
    if (args.init) {
      // State init cell for the contract.
      const stateInitCell = beginCell()
        .store(storeStateInit(args.init))
        .endCell();
      // Convert the state init cell to boc base64.
      stateInit = stateInitCell.toBoc().toString("base64");
    }
    // The payload for the message.
    let payload: string | undefined;
    if (args.body) {
      // Convert the message body to boc base64.
      payload = args.body.toBoc().toString("base64");
    }
    // Send the message using the TonConnect UI and wait for the message to be sent.
    await this.provider.sendTransaction({
      validUntil: validUntil,
      from: from,
      messages: [
        {
          address: address,
          amount: amount,
          stateInit: stateInit,
          payload: payload,
        },
      ],
    });
  }
}
const CONTRACT_ADDRESS = "EQAYLhGmznkBlPxpnOaGXda41eEkliJCTPF6BHtz8KXieLSc";
const getCounterInstance = async () => {
  const client = new TonClient({
    endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC",
  });
  // OR you can use createApi from @ton-community/assets-sdk
  // import {
  //   createApi,
  // } from "@ton-community/assets-sdk";
  // const NETWORK = "testnet";
  // const client = await createApi(NETWORK);
  const address = Address.parse(CONTRACT_ADDRESS);
  const counterInstance = client.open(Counter.createFromAddress(address));
  return counterInstance;
};
export const Header = () => {
  const [tonConnectUI, setOptions] = useTonConnectUI();
  const wallet = useTonWallet();
  const increaseCount = async () => {
    const counterInstance = await getCounterInstance();
    const sender = new TonConnectProvider(tonConnectUI);
    await counterInstance.sendIncrease(sender, {
      increaseBy: 1,
      value: toNano("0.05"),
    });
  };
  const getCount = async () => {
    const counterInstance = await getCounterInstance();
    const count = await counterInstance.getCounter();
    console.log("count", count);
  };
  return (
    <main>
      {!wallet && (
        <button onClick={() => tonConnectUI.openModal()}>Connect Wallet</button>
      )}
      {wallet && (
        <>
          <button onClick={increaseCount}>Increase count</button>
          <button onClick={getCount}>Get count</button>
        </>
      )}
    </main>
  );
};
Jettons와 NFT를 위한 래퍼
Jettons나 NFT와 상호작용하려면 assets-sdk를 사용할 수 있습니다. 이 SDK는 이러한 자산과의 상호작용을 단순화하는 래퍼를 제공합니다. 실용적인 예제는 examples 섹션을 확인하세요.
API 문서
예제
- 단계별 TON Hello World 가이드 - React UI로 간단한 DApp 만들기.
- Demo dApp - @tonconnect/ui-react를 사용한 DApp 예제.
- ton.vote - TON Connect 구현이 포함된 React 웹사이트 예제.