App2App API

이 튜토리얼은 App2App REST API를 손쉽게 사용할 수 있도록 작성된 개발자 가이드입니다. SDK도 기본적으로 REST API 기반이므로 아래 내용을 참고하실 수 있습니다.

Kaia는 기존의 Klaytn과 Finschia 블록체인 네트워크가 통합되어 운영되는 블록체인의 새 이름입니다. 이에 따라 본 문서는 대부분 Klaytn을 Kaia로 / KLAY를 KAIA로 지칭하지만, 하위 호환성을 위해 기존 호출과 응답에서 사용되던 klaytn, KLAY 등의 키워드는 동일하게 유지되는 점 참고 부탁드립니다.

App2App REST API Tutorial

환경 설정

App2App API는 별도의 가입 절차가 필요없고 기본적으로 HTTP 통신이 가능한 어느 환경에서도 동작 가능합니다. 다만, 클립 앱을 실행하여 사용자의 승인을 받아야하기 때문에 기본적으로 클립 앱이 설치된 환경에서 호출 해야합니다.

현재 샌드박스 환경은 따로 제공되지 않으며 실제 카카오 계정으로만 구현 및 테스트가 가능합니다.

아래 예제는 REST API는 curl 명령을 수행하고, Deep Link는 각 모바일 환경의 SDK에서 제공하는 API를 호출하거나, 모바일웹 환경의 경우 Web2App 라이브러리를 이용하여 수행할 수 있습니다. Web2App 라이브러리는 아래 GitHub 저장소를 통해서 얻을 수 있습니다. 자세한 사용 방법은 해당 사이트를 참조하십시오.

Step 1 : Prepare

App2App 처리 시 가정 먼저 수행해야할 Prepare 과정은 Klip에서 처리할 요청 데이터를 전달하고 Request Key를 발급받는 과정입니다. Reqeust Key는 Deep Link 호출 및 결과 확인 과정에서 필요합니다.

요청의 종류는 authtransaction이 있으며, transaction은 다시 코인/토큰 전송 트랜잭션, 카드 전송 트랜잭션, 그리고 컨트랙트 실행 트랜잭션으로 나뉩니다. 필요에 따라서 적절한 필드를 아래와 같이 설정하여 API를 호출합니다.

트랜잭션 요청에서 공통으로 사용하는 from 필드에는 트랜잭션에 서명하는 Klip 사용자의 계정 주소를 설정합니다. 이 필드는 Optional이지만 BApp에서 의도한 사용자와 실제 Klip 사용자가 서로 일치하는지 검사하기 위한 필드이므로 설정하는 것을 권장합니다.

트랜잭션 수수료를 BApp에서 대신 지불하고자 할 경우 수수료 대납 가능한 KAS (Klaytn API Service) 서비스를 구독하여야 하며, 요청 데이터의 BApp 필드 내부에 인증 정보를 함께 전달해야 합니다.

{"bapp": {"name" :  "My BApp", "kas_authorization_key": "Basic abcdefghijklmnopqrstuvwxyz0123456789="}}

KAS 대납 서비스 구독 및 authorization key 발급 방법은 문서를 참고하시기 바라며, 문의사항은 KAS 헬프센터를 이용하여 주시기 바랍니다.

Case 1) Auth 요청

Auth 요청은 Klip 사용자의 EOA를 얻야할 때 사용합니다. 요청 예제는 아래와 같습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "callback": { "success": "mybapp:\/\/klipwallet\/success", "fail": "mybapp:\/\/klipwallet\/fail" }, "type": "auth" }' \
-H "Content-Type: application/json"

API 요청 헤더에 Authorization, Cookie 등의 필드를 넣으면 CORS 설정 때문에 에러가 발생할 수 있습니다. 문서에 설명된 필드 이외에 추가로 넣지 않도록 유의해야합니다.

요청이 정상 처리되면 아래 값을 받습니다.

{
  "request_key": "0b0ee0ad-62b3-4146-980b-531b3201265d", // random string
  "status": "prepared", // 정상적으로 처리된 경우. 만약 문제가 있다면 "error" 상태가 넘어옴.
  "expiration_time": 1600011054, //unix timestamp
  "estimated_gas": 210000 // 트랜잭션을 발생시키는 요청이면서 from 값이 존재할 경우 실제 트랜잭션 생성에 사용될 gasLimit 값을 반환. 이외의 경우 생략.
}

callback 객체의 success 필드에는 Request 처리 성공 시 다시 BApp으로 되돌아올 Deep Link를 설정합니다. fail의 경우는 처리에 실패했을 때 되돌아올 Deep Link를 설정합니다. 예를 들면, 사용자가 승인했으나 이미 완료된 경우, 만료된 request key를 사용한 경우, 잔고 부족 등에 해당합니다. Android 플랫폼의 경우, Intent Scheme 형식(intent://...)의 Deep Link로 작성이 필요합니다.

BApp에서 Deep Link를 지원하지 않는 경우 세팅하지 않아도 됩니다. 이때는 Klip에서 BApp으로 다시 명시적으로 되돌아가야 처리가 완료됨을 안내합니다.

취소 혹은 X 버튼의 경우 callback이 동작하지 않습니다. 사용자가 명시적으로 앱 동작을 취소하거나 클립 앱 종료를 선택했기 때문입니다. 이에 따라 자동 화면 전환이 되지 않습니다.

Case 2) Send Token 요청

Send Token 요청은 Klip 사용자의 토큰을 지정된 주소로 보낼 때 사용합니다. 요청 예제는 아래와 같습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "chain": "klaytn", "type": "send_token", "transaction": { "contract": "0xdc8c8d2CD5829dE8e8a31Fc595D69c4B403e9dD8", "from": "0xcD1722f2947Def4CF144679da39c4C32bDc35681", "to": "0x85c17299e9462e035c149847776e4edb7f4b2aa9", "amount": "100" } }' \
-H "Content-Type: application/json"

Klaytn 체인은 현재 Kaia 체인으로 운영되고 있지만 하위 호한성을 위해 kaia 대신에 klaytn 이름을 사용합니다.

transaction 객체의 contract 필드에는 토큰의 컨트랙트 주소(SCA)를 설정해야 합니다(네이티브 코인의 경우 0x0000000000000000000000000000000000000000 입력). amount 필드에는 보낼 토큰의 양을 설정합니다. 소수점 6자리까지만 지원합니다.

요청이 정상 처리되면 Auth 요청 예시와 동일한 형태의 값을 받습니다.

Case 3) Send Card 요청

Send Card 요청은 Klip 사용자의 카드를 지정된 주소로 보낼 때 사용합니다. 요청 예제는 아래와 같습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "type": "send_card", "transaction": { "contract": "0xB21F0285d27beb2373ECB5c17E119ccEAd7Ee10A", "from": "0xcD1722f2947Def4CF144679da39c4C32bDc35681", "to": "0x85c17299e9462e035c149847776e4edb7f4b2aa9", "card_id": "1234" } }' \
-H "Content-Type: application/json"

transaction 객체의 contract 필드에는 Klip에서 제공하는 카드의 컨트랙트 주소(SCA)를 설정해야 합니다. card_id 필드에는 보낼 토큰의 고유번호를 설정합니다.

요청이 정상 처리되면 Auth 요청 예시와 동일한 형태의 값을 받습니다.

Case 4) Execute Contract 요청

Execute Contract 요청은 Klip 사용자의 서명으로 스마트 컨트랙트 함수를 수행할 때 사용합니다. 요청 예제는 아래와 같습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "chain": "klaytn", "type": "execute_contract", "transaction": { "to": "0xd4fFbe967c31C29199478Be2b5A53dC69eF9B825", "value": "0", "abi": "{ \"constant\": false, \"inputs\": [ { \"name\": \"a\", \"type\": \"string\" } ], \"name\": \"testString\", \"outputs\": [], \"payable\": false, \"stateMutability\": \"nonpayable\", \"type\": \"function\" }", "params": "[\"test_string\"]" } }' \
-H "Content-Type: application/json"

tuple 타입의 경우는 다음의 예제처럼 사용합니다.

curl -X POST 'https://a2a-api.klipwallet.com/v2/a2a/prepare' \
-d '{
    "bapp": {
        "name": "name",
        "callback": {}
    },
    "chain": "klaytn",
    "type": "execute_contract",
    "transaction": {
        "to": "0xF857Dcd31A2a69764bfEbb30dc51EA24519b8aEc",
        "value": "0",
        "abi": "{\"inputs\": [{\"components\": [{\"internalType\": \"string\",\"name\": \"text\",\"type\": \"string\"},{\"internalType\": \"bool\",\"name\": \"completed\",\"type\": \"bool\"}],\"internalType\": \"struct Test.Todo\",\"name\": \"a\",\"type\": \"tuple\"}],\"name\": \"testStruct\",\"outputs\": [],\"stateMutability\": \"nonpayable\",\"type\": \"function\"}",
        "params": "[[\"2001\",false]]"
    }
}' \
-H 'Content-Type: application/json' 

Klaytn 체인은 현재 Kaia 체인으로 운영되고 있지만 하위 호한성을 위해 kaia 대신에 klaytn 이름을 사용합니다.

transaction 객체의 to 필드에는 실행할 컨트랙트의 주소(SCA)를 설정합니다. value 필드에는 전송할 KAIA(KLAY)를 kei(peb) 단위로 설정합니다. payable 함수인 경우에만 설정할 수 있습니다. abi에는 실행할 함수의 ABI를 입력합니다. params에는 해당 함수를 실행한 인자를 배열 형태의 문자열을 설정합니다. abiparams 필드는 String 타입임을 유의하여 전송합니다.

요청이 정상 처리되면 Auth 요청 예시와 동일한 형태의 값을 받습니다.

Case 5) Sign Message 요청

Sign Message 요청은 Klip 사용자의 계정으로 메시지를 서명할 때 사용합니다. 요청 예제는 아래와 같습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "chain": "klaytn", "type": "sign_message", "message": { "value": "original message", "from": "0x220AD25E31BBF7c19D95Be0e47d4cdc0Ad8f8FEa" } }' \
-H "Content-Type: application/json"

원문이 hex encoding 된 데이터를 사용할 경우 다음처럼 사용 할 수 있습니다.

curl -X POST "https://a2a-api.klipwallet.com/v2/a2a/prepare" \
-d '{"bapp": { "name" : "My BApp" }, "chain": "klaytn", "type": "sign_message", "message": { "is_hex_encoded": true, "value": "0x1290fe99a322", "from": "0x220AD25E31BBF7c19D95Be0e47d4cdc0Ad8f8FEa" } }' \
-H "Content-Type: application/json"

Klaytn 체인은 현재 Kaia 체인으로 운영되고 있지만 하위 호한성을 위해 kaia 대신에 klaytn 이름을 사용합니다.

message 오브젝트의 value 필드에 서명할 원문을 포함합니다. from 필드는 서명하는 계정 주소입니다. 선택 항목이라 생략할 수 있으나, 만약 입력한다면 해당 값으로 서명 계정의 주소가 올바른지 검증합니다.

요청이 정상 처리되면 Auth 요청 예시와 동일한 형태의 값을 받습니다.

원문에 아스키코드 이외의 문자가 포함된 경우에는, caver-js의 caver.utils.hashMessage 메소드를 통해서 해싱한 결과물을 Klip에서 응답한 signature 값으로 recover하면 결과가 실제 서명한 계정과 다른 주소로 응답될 수 있는 부분 참고하시기 바랍니다.

원문이 ‘0x’로 시작 할 경우 caver-js의 caver.utils.hashMessage 메소드를 통해서 해싱한 결과물은 자동으로 hex encoding 된 데이터로 판단하여 생성합니다. 동일 데이터에 대해 Klip에서 “is_hex_encoded": true 사용하여 signature 를 만들지 않으면 recover 한 결과가 실제 서명한 계정과 다른 주소로 응답될 수 있는 부분 참고하시기 바랍니다.

Step 2 : Request

Request는 BApp에서 클립 앱에 App2App 처리를 요청하기 위한 Deep Link를 실행하는 과정입니다. Deep Link를 통해 클립 앱이 실행된 경우, 사용자에게 확인 창이 뜨게됩니다. 인증의 경우는 BApp에 EOA를 제공에 동의를 구하는 창이 뜨게되며, 트랜잭션 처리의 경우 요청한 트랜잭션 데이터를 화면에 보여주고, pin code 입력받아서 실제 트랜잭션을 처리하게됩니다.

만약 Prepare 과정에서 callback Deep Link를 설정한 경우, BApp으로 자동으로 넘어갑니다. 설정하기 않은 경우 사용자에게 BApp으로 되돌아가기 위한 안내 메시지를 제공합니다.

Klip에서 제공하는 URL은 아래와 같습니다. 공통적으로 request_key를 쿼리 스트링으로 받습니다. Prepare 과정에서 얻은 값을 설정합니다.

https://klipwallet.com?target=/a2a?request_key=0b0ee0ad-62b3-4146-980b-531b3201265d

위 Deep Link를 통해 클립 앱을 실행하거나 설치할 수 있도록 유도됩니다.

Step 3 : Result

App2App API 요청의 최종 상태는 아래와 같이 Result API를 polling하여 얻을 수 있습니다. Prepare 과정에서 얻은 request_key값을 쿼리 스트링으로 설정합니다.

curl -X GET "https://a2a-api.klipwallet.com/v2/a2a/result?request_key=0b0ee0ad-62b3-4146-980b-531b3201265d" \
-H "Content-Type: application/json"

요청이 정상 처리되면 요청 type별로 아래 값을 받습니다.

Case 1) Auth 요청

{
  "request_key": "0b0ee0ad-62b3-4146-980b-531b3201265d",
  "expiration_time": 1600011054,
  "status": "completed",
  "result": {
    "klaytn_address": "0x85c17299e9462e035c149847776e4edb7f4b2aa9" // 호환성을 위해 필드명에는 klaytn을 사용하나 Kaia를 포함한 모든 네트워크에 동일한 주소를 갖습니다.
  }
}

Case 2) Sign Message 요청

{
  "request_key":  "0b0ee0ad-62b3-4146-980b-531b3201265d",
  "expiration_time": 1600011054,
  "status": "completed",
  "result": {
    "signature": "0x1dc98165c3fc523bcdbdf18eadba12b004cd30b232e5e65fdd6424412cbf0dab2d131dda838cd249a7d00414ae53abe5ba6fa7bf8446f28c328bc60443c1545d07f5",
    "hash": "0x0b9ac081057767a46500710d1007d4f0de7f23b109b50ffdc60d03b175a9eb6f"
  }
}

result 객체의 signature 필드로 부터 원문에 대한 서명값을 받을 수 있습니다. recover는 caver.utils.recover 메소드 또는 아래 예제 코드를 통해서 수행할 수 있습니다.

const Bytes = require('eth-lib/lib/Bytes');
const elliptic = require("elliptic");
const secp256k1 = new elliptic.ec("secp256k1");
const { keccak256, keccak256s } = require("eth-lib/lib/hash");
const utils = require("caver-js/packages/caver-utils");

function recover(message) {
  const hex = message.signature
  const hash = message.message
  const vals = [Bytes.slice(64, Bytes.length(hex), hex), Bytes.slice(0, 32, hex), Bytes.slice(32, 64, hex)];

  const vrs = { v: Bytes.toNumber(vals[0]), r: vals[1].slice(2), s: vals[2].slice(2) };

  const ecPublicKey = secp256k1.recoverPubKey(new Buffer(hash.slice(2), "hex"), vrs, vrs.v < 2 ? vrs.v : 1 - vrs.v % 2); // because odd vals mean v=0... sadly that means v=0 means v=1... I hate that
  const publicKey = "0x" + ecPublicKey.encode("hex", false).slice(2);
  const publicHash = keccak256(publicKey);

  const recoveredAddress = "0x" + publicHash.slice(-40);

  if (recoveredAddress.toLowerCase() === message.address.toLowerCase()) {
    console.log("ok", recoveredAddress, message.address)
    return true
  } else {
    console.log("not match", recoveredAddress, message.address)
  }
  return false
}

const EthereumPrefix = "Ethereum Signed Message"
const PolygonPrefix = "Ethereum Signed Message"
const KlaytnPrefix = "Klaytn Signed Message"

function hashMessage(chainPrefix, data) {
  const message = utils.isHexStrict(data) ? utils.hexToBytes(data) : data
  const messageBuffer = Buffer.from(message)
  const preamble = `\x19` + chainPrefix + `:\n${message.length}`
  const preambleBuffer = Buffer.from(preamble)
  const saltedMessage = Buffer.concat([preambleBuffer, messageBuffer])
  return keccak256(saltedMessage)
}

const message = {
  message: hashMessage(PolygonPrefix, 'original message'),
  address: "0xa99694791182d3f6d0e0ccf5a2b0703845a50a50",
  signature: "0x25b46420d40415d1a3cab6d1b2849e93e100fb310eebc9db8580a6eb465fc3535dd7f61c6a95efe281acfe0a9de6f089eec57ea78c578738e1dc16ded1c7999d1c"
}

recover(message)

위의 예시는 type을 sign_message로 설정한 경우로, sign_message_eip191을 사용하는 경우에는 Kaia의 표준에 따라 "\x19Ethereum Signed Message:\n" + len(message)의 접두사를 붙여 메시지에 서명합니다. Kaia에서 변경된 메시지 서명 표준의 변화에 대해서는 여기를 참고하시기 바랍니다.

원문에 아스키코드 이외의 문자가 포함된 경우에는, caver-js의 caver.utils.hashMessage 메소드를 통해서 해싱한 결과물을 Klip에서 응답한 signature 값으로 recover하면 결과가 실제 서명한 계정과 다른 주소로 응답될 수 있는 부분 참고하시기 바랍니다.

원문이 ‘0x’로 시작 할 경우 caver-js의 caver.utils.hashMessage 메소드를 통해서 해싱한 결과물은 자동으로 hex encoding 된 데이터로 판단하여 생성합니다. 동일 데이터에 대해 Klip에서“is_hex_encoded": true 사용하여 signature 를 만들지 않으면 recover 한 결과가 실제 서명한 계정과 다른 주소로 응답될 수 있는 부분 참고하시기 바랍니다.

Case 3) Auth, Sign Message 이외의 요청

{
  "request_key": "0b0ee0ad-62b3-4146-980b-531b3201265d",
  "expiration_time": 1600011054,
  "status": "completed",
  "result": {
    "tx_hash": "0x82d018556e88b8f8f43dc2c725a683afc204bfd3c17230c41252354980f77fb3",
    "status": "success"
  }
}

Auth가 아닌 다른 요청 타입일 경우 result 객체가 추가로 넘어옵니다. result 객체의 tx_hash 필드를 복사해 Kaiascope(=Klaytnscope), Etherscan, Polygon 등에서 사용하면 트랜잭션의 처리 상태에 관한 자세한 정보를 확인할 수 있습니다. result 객체의 status 필드는 pending 상태의 경우, 사용자가 Klip에서 확인했자만, 체인에서 아직 트랜잭션을 처리하는 중임을 의미합니다. 일반적으로 몇 초 후에 다시 확인하면 요청을 확인할 수 있습니다. success는 요청이 성공적으로 처리될 때, fail은 요청 처리에 실패할 때의 상태입니다.

Get Additional Information

일반적으로 BApp에서 카드를 전송하기 위해서는 사용자가 보유하고 있는 카드 목록을 조회하여 고유번호를 얻어야하는 경우가 많습니다. Auth 과정을 통해서 얻은 EOA와 조회하려는 컨트랙트의 주소를 인자로 삼아서 해당 EOA가 컨트랙트에 소유하고 있는 카드 목록을 조회할 수 있습니다.

요청 예제는 아래와 같습니다.

curl -X GET "https://a2a-api.klipwallet.com/v2/a2a/cards?chain=klaytn&sca=0xB21F0285d27beb2373ECB5c17E119ccEAd7Ee10A&eoa=0x85c17299e9462e035c149847776e4edb7f4b2aa9&cursor=" -H "Content-Type: application/json"

요청이 정상 처리되면 아래 값을 받습니다.

{
    "name": "conan",
    "symbol_img": "https://media.klipwallet.com/token_icon/klay_klip.svg",
    "cards": [
    {
      "created_at": 1580176787,
      "created_at_format": "format",
      "updated_at": 1580176787,
      "updated_at_format": "format",
      "owner": "0x85c17299e9462e035c149847776e4edb7f4b2aa9",
      "sender": "0x2412b300750f505fb2e68ddf0cd45e9d95f5378d",
      "sender_kakao_id": "1234"
      "card_id": 19,
      "card_uri": "https://media.klipwallet.com/card-asset/1234/19.json",
      "transaction_hash": "0x293a2e53ecf238109908e65a2b7ff4aad0919ce3ce54af08d6fc4323f28e935d"
    },
    ],
    "next_cursor": "mrzedXOE9OeEorkAvwQXB7JdVg4LP1Rzze2kLQFxLU4C8iMOhOVulzIr5iesZoie9uv9h87UNXsWCKdhqYszXFWLsYYI7h125Rx8p56qlMKaZ20YbNW3zDGmNBJKM1wL"
}

카드 정보가 정상 조회되었다면 계정이 이 BApp에서 소유한 카드 목록과 정보를 받습니다.

  • cards에서 card는 이 BApp에서 쓰이는 카드입니다. BApp에는 카드 1종류가 들어있습니다.

  • Query 파라미터로 cursor 또는 isAll 둘 중 하나만 사용해야 합니다(isAllfalse이면 cursor를 사용할 수 있습니다).

  • cursor를 사용하면 Pagination을 사용합니다.

    • 1회 요청에 최대 카드 100개의 정보를 받습니다.

    • 정보를 불러울 카드가 100개를 초과 시 다음 카드 정보를 불러올 수 있는 커서값인 next_cursor로 나머지 카드 정보를 받습니다.

    • 나머지 카드 정보를 받으려면 cursor에 이전 호출에서 받은 next_cursor를 넣고 API를 다시 호출합니다.

위 예시에서 cards.next_cursor값이 존재하므로 이 계정은 conan 카드를 100개 이상 가지고 있습니다. 한 번에 조회할 카드 개수가 100개를 초과한다면 1회 호출 시 카드 100개 정보만 받고 cards.next_cursor값을 받습니다. 나머지 카드 정보를 조회하려면 cards.next_cursor값을 Query 파라미터 cursor에 전달하고 API를 다시 호출해야 합니다.

예를 들어, 정보를 불러올 카드 개수가 150개라면, 먼저 API를 호출하여 카드 100개의 정보와 cards.next_cursor값을 받습니다. 그리고 동일한 API를 다시 호출할 때 cards.next_cursor값을 Query 파라미터 cursor로 사용하면 나머지 50개의 정보를 받습니다.

요청이 정상적으로 처리되지 않은 경우 HTTP 400 또는 500이 리턴되며 자세한 내용은 Basics를 참조하십시오.

이 문서 혹은 Klip에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

Last updated