본문으로 건너뛰기
전체 글

같은 Solana 토큰인데 왜 조회만 가능할까

· 약 9분
Bankware Global Engineering

새 Solana 토큰을 등록했다. 잔액 조회는 된다. A가 100개를 가지고 있다는 응답도 받았다. 그런데 A에서 B로 10개를 보내려 하자 전송 API가 보이지 않는다. 잔액을 읽을 수 있다면 전송도 할 수 있어야 하는 것 아닐까?

토큰을 읽는 데 필요한 정보와, 그 토큰의 규칙에 맞는 거래를 만드는 데 필요한 정보는 다르다. Token-2022에서는 이 차이가 특히 잘 드러난다. 같은 토큰 프로그램을 사용해도 어떤 확장 기능이 붙었는지에 따라 전송 조건이 달라지기 때문이다.

멀티체인 API의 공통점과 차이에서는 체인마다 남겨야 할 조건을 살펴봤다. 이번에는 같은 Solana 안으로 들어가, BXB가 토큰별로 어떤 명령을 제공할지 판단하는 과정을 따라가 보자. 설명 범위는 토큰 프록시의 조회와 표준 쓰기 명령이며, 자원 관리 API 전체의 지원 범위를 뜻하지 않는다.

1. 잔액 100개가 보여도 전송 요청은 멈출 수 있다​

토큰 T의 소수 자릿수는 0이고, A와 B의 토큰 계정에는 각각 100개와 0개가 있다고 하자. 두 계정은 이미 만들어져 있으며, 요청자는 BXB에서 이 네트워크를 조회할 권한이 있다. 목표는 A의 토큰 10개를 B로 보내는 것이다.

다만 T에는 확인한 BXB 빌드가 아직 이름을 해석하지 못하는 확장 종류가 포함돼 있다고 가정한다. 체인에서는 유효한 토큰이지만, 연동 소프트웨어가 그 확장을 아직 모르는 상황이다. 이는 특정 운영 토큰에서 관측한 장애가 아니라, 코드의 판단을 설명하기 위한 가상 사례다. 등록에 필요한 기본 계정 정보와 표시 정보는 정상적으로 제공된다고 가정한다.

이때 BXB는 T의 기본 정보를 읽고 등록할 수 있으며, 토큰 계정의 기본 잔액도 조회할 수 있다. A의 잔액 응답에서 주소 필드를 생략하고 수량 부분만 보면 다음과 같다.

{
"exists": true,
"amount": "100",
"decimals": 0
}

amount는 원장의 정수 단위를 십진수 문자열로 표현한 값이다. 이 예제에서는 소수 자릿수만 적용한 기본 환산값인 100개로 표시한다. 이 응답은 기본 잔액 필드에 무엇이 기록돼 있는지 알려 준다. 100개 전부가 아무 조건 없이 전송 가능하다는 판단까지 포함하지는 않는다.

BXB는 모르는 확장이 있는 T에 대해 표준 쓰기 명령을 제공하지 않는다. 따라서 이번 10개 전송은 서명과 체인 제출로 이어지지 않는다. 다른 거래가 없다면 결과는 A 100 / B 0이고, 이 요청에 대한 온체인 거래 수수료도 발생하지 않는다. 송금이 제출된 뒤 실패한 상황과는 출발점이 다르다.

2. Token-2022는 토큰마다 새 프로그램을 만드는 방식일까​

Solana의 토큰 구조에서는 세 역할을 먼저 나눠 볼 필요가 있다. 토큰 프로그램은 발행·전송·소각 같은 명령을 실행하는 코드다. Mint 계정은 토큰 한 종류의 총발행량, 소수 자릿수, 발행 권한 등을 담는다. 토큰 계정은 특정 Mint의 토큰을 누가 얼마나 보유하는지 기록한다. 예제의 T는 Mint로 식별하고, A와 B의 보유량은 각자의 토큰 계정에서 읽는다.

기존 Token Program과 Token-2022는 서로 다른 프로그램 주소를 사용한다. Token-2022는 공통 토큰 기능에 확장을 더할 수 있게 만든 프로그램이다. 토큰마다 새 전송 프로그램을 직접 배포해야 확장 기능을 얻는 구조는 아니다. Token-2022 공식 설명은 기본 Mint의 앞 82바이트와 토큰 계정의 앞 165바이트가 기존 형식과 호환된다고 설명한다.

기본 정보 뒤에는 전송 수수료, 전송 불가, 추가 프로그램 호출 같은 규칙을 표현하는 확장 정보가 붙을 수 있다. Mint에 붙는 확장과 개별 토큰 계정에 붙는 확장도 구분된다. 공통 필드를 읽는 코드가 동작한다고 해서, 그 뒤에 추가된 모든 규칙을 이해했다는 뜻은 아니다.

계정을 찾는 방법도 토큰 프로그램을 고려해야 한다. BXB의 잔액 조회는 소유자·Mint·토큰 프로그램에 대응하는 연관 토큰 계정, ATA를 찾아 기본 수량을 읽는다. Token-2022인데 기존 Token Program을 기준으로 ATA를 계산하면 다른 주소를 찾게 된다. 같은 사용자와 같은 토큰이라고 생각해도, 어떤 프로그램이 그 계정을 관리하는지까지 맞춰야 한다.

이 조회는 해당 ATA의 기본 잔액을 읽는 경로다. 사용자가 가진 모든 임의의 토큰 계정을 합산하거나, 확장에 들어 있는 별도 잔액과 표시 규칙까지 모두 반영한 보고서로 확대해서는 안 된다.

3. 모르는 확장을 만나도 기본 정보는 읽을 수 있다​

확장 데이터는 종류와 길이를 함께 기록한다. 이 방식을 TLV(Type-Length-Value)라고 부른다. 어떤 종류인지, 내용이 몇 바이트인지, 실제 내용이 무엇인지 순서대로 놓는다. BXB가 읽는 Mint 확장 구역의 항목은 다음처럼 구성된다.

종류: 2바이트
길이: 2바이트
내용: 길이에 적힌 바이트 수
다음 확장 항목으로 이동

BXB는 등록 과정에서 확장 항목의 종류 번호를 수집한다. 종류와 길이를 읽어 다음 항목으로 이동하므로, 각 확장의 내용을 모두 해석해야만 확장 목록을 얻는 것은 아니다. 이 방식으로 T의 기본 정보를 읽으면서도 해석하지 못한 확장이 존재한다는 사실을 보존할 수 있다.

확장 목록은 이름 대신 번호로 저장된다. 현재 라이브러리에 이름이 없는 번호도 없었던 것처럼 버리지 않기 위해서다. 번호를 읽었다는 것은 그 확장의 내용을 이해했다는 뜻이 아니다. 이후 단계에서 해당 빌드가 그 번호를 아는지와, 어떤 명령을 제한하는지를 별도로 판단한다.

여기에는 작은 차이도 중요하다. 확장을 확인했는데 하나도 없는 상태와 확장 목록을 기록한 적이 없는 상태는 다르다. 확인한 BXB 구현은 전자를 빈 목록으로 저장하고, 후자는 스냅샷 누락으로 취급해 표준 쓰기를 보류한다. 정보가 없다는 이유로 확장이 없는 일반 토큰이라고 추정하지 않는 것이다.

등록이 성공하려면 Mint를 관리하는 프로그램, 초기화 여부, 기본 데이터와 필요한 표시 정보 등의 조건도 맞아야 한다. 모르는 확장을 보존하는 기능을 잘못된 계정이나 모든 형태의 데이터를 무조건 받아들이는 기능으로 해석할 수는 없다.

4. 전송 불가와 미지원은 다음 행동이 다르다​

이제 이름을 아는 확장의 경우를 비교해 보자. 아래는 서로 다른 토큰 구성에 적용하는 정적 판단의 예다. T에 이 기능을 차례로 추가하거나, 모든 확장을 한 Mint에 함께 넣는 예제가 아니다. 실제로 함께 사용할 수 없는 확장 조합도 있다. Solana 확장 문서는 확장별 적용 대상과 조합 제약을 설명한다.

첫째, NonTransferable은 계정 사이의 토큰 전송을 금지하는 규칙이다. BXB는 일반 전송과 transferChecked, 위임받아 전송하는 두 변형까지 전송 명령 네 가지를 제외한다. 이는 BXB가 구현을 추가하면 같은 규칙의 토큰을 자유롭게 보낼 수 있다는 뜻이 아니다. 반면 전송 금지가 발행과 소각을 포함한 모든 변경 금지를 뜻하지는 않는다. Non-Transferable 공식 문서도 권한에 맞는 발행·소각과 전송을 구별한다.

둘째, TransferHook은 전송 과정에 별도 프로그램의 처리를 연결하는 확장이다. 거래를 구성할 때 그 프로그램이 요구하는 추가 계정도 찾아 넣어야 한다. 이 연결 과정은 Transfer Hook 연동 안내에 설명돼 있다. 확인한 BXB 구현은 이 추가 계정 해석 경로를 아직 제공하지 않으므로 전송 명령 네 가지를 제외한다. 이 경우는 토큰의 전송 금지 규칙과 달리 연동 구현의 부족이 이유다.

셋째, TransferFeeConfig가 있으면 단순 transfer와 위임 전송인 transferFrom을 제외하지만, Mint와 소수 자릿수를 함께 확인하는 transferChecked 계열은 남긴다. 전송 전부가 불가능한 것이 아니라 필요한 정보를 담은 명령을 골라야 하는 경우다. Checked를 선택해도 토큰의 전송 수수료 규칙이 사라지지는 않는다. Transfer Fees 공식 문서는 TransferChecked에서도 설정된 수수료가 적용된다고 설명한다.

이 차이를 정리하면 다음과 같다. 다른 제한은 없다고 가정한 각 구성의 판단이다.

구성제외하는 명령이유
전송 불가 확장전송 네 가지토큰 규칙
전송 hook 확장전송 네 가지연동 미구현
전송 수수료 확장일반 전송 두 가지토큰 규칙
모르는 확장 종류표준 쓰기 전체해석 미지원

코드에서는 토큰 프로그램의 규칙에 따른 제한을 PROGRAM_RULE, 이 구현이 아직 다루지 못하는 경우를 NOT_IMPLEMENTED로 구분한다. 앞의 경우에는 요청 목적이나 명령 선택을 바꿔야 하고, 뒤의 경우에는 필요한 해석과 거래 구성이 지원되는지 확인해야 한다. 같은 “보낼 수 없음”이라도 재시도만으로 해결할 일은 아니다.

5. 토큰별 지원 판단을 명세와 실행 양쪽에 적용한다​

BXB는 발행·전송·소각·위임·권한 변경 등을 포함한 17개의 표준 쓰기 명령을 토큰 프록시의 기본 목록으로 둔다. 확장마다 제한하는 명령을 모으고, 그 명령을 기본 목록에서 제외한다. 여러 확장이 붙으면 각 확장의 제한을 합친다. 한 확장이 허용한다고 해서 다른 확장이 금지한 명령을 다시 허용하지 않는다.

T에는 모르는 확장 종류가 있으므로 이 쓰기 목록이 비게 된다. 그렇다고 읽기 API까지 함께 사라지지는 않는다. Mint 정보, 잔액, 토큰 계정 상태, 보유 계정 목록을 읽는 경로는 쓰기 명령 목록과 별도로 생성한다. 거래 상태 조회 역시 별도다. 보유 계정 목록에는 반환 개수 제한도 있다.

이 판단은 OpenAPI 명세에 반영된다. T의 명세에는 지원하는 읽기 경로를 남기고, 사용할 수 없는 표준 쓰기 경로는 넣지 않는다. 사용자가 Swagger 화면에서 지원하지 않는 전송을 정상 기능처럼 선택하는 일을 줄이는 방식이다. ABI와 HTTP 입력의 연결을 다룬 BXB API Factory 글에서 한 걸음 더 들어가, 이번에는 무슨 명령을 명세에 실을지를 토큰의 구성에 맞춰 결정한다.

명세에서 경로를 숨기는 것으로 실행 검사가 끝나지는 않는다. BXB의 실행 서비스도 자원에 저장된 확장 목록으로 해당 명령이 가능한지 다시 판단한다. 이 검사는 서명에 사용할 키 자료를 조회하고 거래를 구성하는 단계보다 앞에 있다. 따라서 T에 대한 전송 요청을 서비스에 직접 전달해도 지원 판단을 통과해야 한다.

이 재검사는 등록된 확장 스냅샷을 기준으로 한 검사다. 매 요청마다 체인에서 모든 확장 내용과 최신 설정을 새로 읽는다는 뜻은 아니다. 또한 읽기 경로가 남아 있다는 것과 누구나 읽을 수 있다는 것도 다르다. BXB의 조회 서비스는 요청자의 userKey가 해당 네트워크의 허가된 지갑에 연결되는지 확인한다.

6. 허용된 명령도 이번 거래의 성공을 보장하지 않는다​

어떤 토큰에 mintTo 경로가 있다고 하자. 명세에 발행 명령이 보인다는 사실만으로 현재 요청자가 발행 권한을 가진 것은 아니다. 발행 권한이 바뀌거나 제거됐을 수도 있다. BXB는 이런 명령을 준비할 때 체인에서 현재 권한을 읽어 서명 지갑과 대조하는 경로를 둔다.

위임 전송에서도 상황은 비슷하다. transferFrom을 제공할 수 있는 토큰이어도, 실제 소유자가 요청자에게 권한을 맡겼는지와 위임 한도가 요청 수량을 감당하는지는 별도로 확인해야 한다. BXB는 해당 토큰 계정을 읽어 위임받은 주소와 위임 한도를 검사한다.

따라서 세 질문을 나눠야 한다.

질문확인할 내용
이 명령을 제공할 수 있는가확장 종류와 BXB의 명령별 지원 범위
이 요청자가 실행할 수 있는가업무 접근 정책과 현재 체인 권한
이번 거래가 성공했는가제출 뒤의 실제 거래 결과

체인에서 판단하는 현재 계정 상태와 실행 조건도 남아 있다. 지원 목록에 없는 제한을 사전에 모두 찾아냈다고 보장하거나, 이 목록을 통과한 거래가 반드시 성공한다고 설명해서는 안 된다. 확장 이름을 인식한다는 사실 역시 해당 확장의 모든 세부 기능을 제공한다는 뜻은 아니다.

7. 지원 범위는 읽기와 쓰기, 그리고 이유로 표현한다​

처음의 T로 돌아가 보자. BXB는 기본 잔액을 읽어 A 100개, B 0개를 보여 줄 수 있다. 하지만 그 빌드가 해석하지 못한 확장이 있어 표준 쓰기 명령은 보류한다. 따라서 이번 10개 전송은 제출되지 않고, 다른 거래가 없다면 A 100 / B 0이 유지된다.

다음 단계는 잔액 조회가 성공했다는 이유로 전송 요청을 반복하는 것이 아니다. 어떤 확장을 인식하지 못했는지 확인하고, 필요한 명령을 구성할 수 있는 구현과 검증이 갖춰졌는지를 살펴야 한다. 전송 금지 규칙을 가진 토큰이라면 기능 추가가 아니라 토큰을 사용하려는 목적부터 다시 맞춰야 한다.

토큰 프로그램 이름 하나로 지원 여부를 표현하면 이 차이가 사라진다. 무엇을 읽을 수 있는지, 어떤 명령을 만들 수 있는지, 제외한 명령은 왜 제외했는지를 함께 표현해야 업무 시스템도 조회 결과를 올바르게 사용하고 다음 행동을 정할 수 있다.