@nexus-cross/onramp 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ /** 구독 해제 함수. core(connect-kit) 의존을 피하려고 onramp 안에 자체 정의. */
2
+ type Unsubscribe = () => void;
3
+ type OnRampProviderId = 'alchemypay' | (string & {});
4
+ interface OnRampOpenParams {
5
+ /** 사용자 지갑 주소. 미지정 시 use case가 wallet state에서 주입. */
6
+ address?: string;
7
+ /** 구매 대상 암호화폐 심볼. 예: 'CROSS' | 'USDT' | 'ETH' */
8
+ crypto: string;
9
+ /** 네트워크 식별자. provider별 코드는 어댑터가 매핑. */
10
+ network: string;
11
+ /** 법정화폐. 미지정 시 어댑터 기본값. */
12
+ fiat?: string;
13
+ /** 법정화폐 결제 금액. fiatAmount 또는 cryptoAmount 중 하나 권장. */
14
+ fiatAmount?: number;
15
+ /** 암호화폐 수량. 둘 다 지정 시 cryptoAmount 우선. */
16
+ cryptoAmount?: number;
17
+ /** 완료/취소 후 돌아올 URL. 미지정 시 어댑터가 window.location.origin 사용. */
18
+ redirectUrl?: string;
19
+ }
20
+ interface OnRampSession {
21
+ readonly sessionId: string;
22
+ readonly url: string;
23
+ readonly provider: OnRampProviderId;
24
+ readonly openedAt: number;
25
+ }
26
+ type OnRampStatus = 'pending' | 'completed' | 'failed' | 'cancelled';
27
+ interface OnRampStatusEvent {
28
+ sessionId: string;
29
+ status: OnRampStatus;
30
+ txHash?: string;
31
+ errorMessage?: string;
32
+ }
33
+ type OnRampDisallowReason = 'unsupported_country' | 'unsupported_region' | 'sanctioned' | 'kyc_required' | 'provider_outage' | 'unknown';
34
+ interface OnRampEligibility {
35
+ readonly provider: OnRampProviderId;
36
+ readonly allowed: boolean;
37
+ /** ISO 3166-1 alpha-2 (예: 'KR'). 백엔드가 결정. */
38
+ readonly country?: string;
39
+ /** allowed=false일 때 머신-리더블 사유. */
40
+ readonly reason?: OnRampDisallowReason;
41
+ /** 선택적 사람-친화 메시지. */
42
+ readonly message?: string;
43
+ /** 만료 시각(ms epoch). 어댑터는 이때까지 캐시. */
44
+ readonly expiresAt?: number;
45
+ }
46
+ type OnRampErrorCode = 'UNSUPPORTED_NETWORK' | 'UNSUPPORTED_CRYPTO' | 'MISSING_ADDRESS' | 'INVALID_AMOUNT' | 'SIGN_FAILED' | 'POPUP_BLOCKED' | 'PROVIDER_DISABLED' | 'ELIGIBILITY_FAILED';
47
+ declare class OnRampError extends Error {
48
+ readonly code: OnRampErrorCode;
49
+ readonly details?: Record<string, unknown>;
50
+ constructor(code: OnRampErrorCode, message: string, details?: Record<string, unknown>);
51
+ }
52
+ /** UI / use case 양쪽에서 알 수 없는 reason 문자열 정규화. */
53
+ declare function normalizeDisallowReason(value: unknown): OnRampDisallowReason;
54
+
55
+ /**
56
+ * Provider-중립 fiat→crypto 온램프 추상화. AlchemyPay가 1차 어댑터,
57
+ * 추후 Moonpay/Transak 등 동일 인터페이스로 확장.
58
+ *
59
+ * 구현체는 어댑터 레이어가 담당 — 이 Port는 도메인 계약만 가진다.
60
+ */
61
+ interface OnRampPort {
62
+ readonly provider: OnRampProviderId;
63
+ /**
64
+ * 국가/지역/제재 기반 권한 사전 조회.
65
+ *
66
+ * 실패(네트워크/타임아웃/비표준 응답) 시 fail-closed: throw 대신
67
+ * `{ allowed: false, reason: 'unknown' }`을 반환해야 한다. 가용성보다
68
+ * 컴플라이언스 안전 마진을 우선한다.
69
+ *
70
+ * 어댑터는 응답 `expiresAt` 또는 자체 fallback TTL로 결과를 캐싱한다.
71
+ */
72
+ getEligibility(ctx?: {
73
+ network?: string;
74
+ }): Promise<OnRampEligibility>;
75
+ /** 결제 위젯을 연다. eligibility 통과 후 호출되는 게 정상. */
76
+ open(params: OnRampOpenParams): Promise<OnRampSession>;
77
+ /** 선택. 진행 중인 세션을 명시적으로 닫는다. */
78
+ close?(sessionId: string): Promise<void>;
79
+ /** 선택. provider 콜백/postMessage 기반 상태 이벤트 구독. */
80
+ onStatus?(cb: (e: OnRampStatusEvent) => void): Unsubscribe;
81
+ }
82
+
83
+ /**
84
+ * 외부 통신을 추상화한 리포지토리. AlchemyPayBrowserAdapter는 fetch를
85
+ * 직접 호출하지 않고 이 인터페이스를 통한다.
86
+ *
87
+ * 구현체:
88
+ * - HttpOnRampRepository: crossx 2.0 embedded-wallet-gateway 호출
89
+ * - MockOnRampRepository: 외부 의존성 0, in-memory + Web Crypto HMAC
90
+ *
91
+ * DApp이 직접 구현해서 주입할 수도 있다 (자체 게이트웨이가
92
+ * 다른 응답 스키마를 쓰는 경우 등). 식별자(projectId 등)는 구현체
93
+ * 생성자에서 주입하므로 메서드 ctx에 포함하지 않는다.
94
+ */
95
+ interface OnRampRepository {
96
+ fetchEligibility(ctx: {
97
+ network?: string;
98
+ }): Promise<OnRampEligibility>;
99
+ requestSignedUrl(payload: {
100
+ params: OnRampOpenParams;
101
+ }): Promise<string>;
102
+ }
103
+
104
+ /**
105
+ * 브라우저 환경용 OnRampPort 구현.
106
+ *
107
+ * 외부 통신은 OnRampRepository로 위임 — 기본은 HttpOnRampRepository(fetch),
108
+ * 테스트/데모에서는 MockOnRampRepository를 주입할 수 있다. 어댑터 자체는:
109
+ * - eligibility 캐시 (expiresAt 또는 fallback TTL)
110
+ * - 도메인 검증 (address required 등)
111
+ * - window.open / OnRampError 변환
112
+ * - 세션 ID 발급
113
+ * - popup closed polling + postMessage 리스너로 status 이벤트 발화
114
+ * 만 담당한다.
115
+ *
116
+ * ⚠️ status 이벤트는 **UX 보조** 수준의 신호. 결제 완료의 진실의 단일 소스는
117
+ * AlchemyPay → DApp 백엔드 webhook이다. cancelled는 popup이 닫힌 사건이지
118
+ * "사용자가 의도적으로 취소"라는 보장은 아니다.
119
+ */
120
+ interface AlchemyPayBrowserAdapterOptions {
121
+ /**
122
+ * crossx 2.0 embedded-wallet-gateway용 식별자. kitConfig.crossProjectId가
123
+ * `X-Project-Id`로 그대로 전달된다. `repository` 옵션을 직접 주입하는
124
+ * 경우(예: Mock) projectId는 무시되어도 무방.
125
+ */
126
+ projectId?: string;
127
+ /** native SDK 흐름에서만 사용. 웹은 생략. */
128
+ appId?: string;
129
+ /** `X-App-Id`와 함께 전송. 'android' | 'ios' | 'windows'. 웹은 생략. */
130
+ appType?: string;
131
+ /**
132
+ * 외부 통신 리포지토리. 미주입 시 projectId로 HttpOnRampRepository를 자동 구성.
133
+ * 테스트/데모에서는 MockOnRampRepository 등을 주입.
134
+ */
135
+ repository?: OnRampRepository;
136
+ /** 응답에 expiresAt 없을 때 fallback TTL. 기본 5분. */
137
+ eligibilityTtlMs?: number;
138
+ windowFeatures?: string;
139
+ /**
140
+ * 신뢰할 postMessage origin 목록. 기본:
141
+ * ['https://ramp.alchemypay.org',
142
+ * 'https://ramptest.alchemypay.org',
143
+ * 'https://ramp-sandbox.alchemypay.org']
144
+ * 추가 도메인이 필요하면 override.
145
+ */
146
+ trustedOrigins?: readonly string[];
147
+ /** popup closed polling 주기. 기본 500ms. */
148
+ popupPollIntervalMs?: number;
149
+ /**
150
+ * popup이 닫힌 직후 `cancelled`로 발화하기 전 대기하는 grace window(ms).
151
+ * 이 시간 동안 postMessage로 종결 status(completed/failed)가 오면 그 status가
152
+ * 우선되고 cancelled는 발화하지 않는다. 기본 300ms.
153
+ */
154
+ cancelGraceMs?: number;
155
+ }
156
+ declare class AlchemyPayBrowserAdapter implements OnRampPort {
157
+ readonly provider: OnRampProviderId;
158
+ private readonly repository;
159
+ private readonly eligibilityTtlMs;
160
+ private readonly windowFeatures;
161
+ private readonly trustedOrigins;
162
+ private readonly popupPollIntervalMs;
163
+ private readonly cancelGraceMs;
164
+ private cache;
165
+ private statusListeners;
166
+ private activeWatch;
167
+ private cancelGraceTimer;
168
+ constructor(opts: AlchemyPayBrowserAdapterOptions);
169
+ getEligibility(ctx?: {
170
+ network?: string;
171
+ }): Promise<OnRampEligibility>;
172
+ private storeCache;
173
+ onStatus(cb: (e: OnRampStatusEvent) => void): Unsubscribe;
174
+ close(sessionId: string): Promise<void>;
175
+ private emit;
176
+ private startWatching;
177
+ private stopWatching;
178
+ open(params: OnRampOpenParams): Promise<OnRampSession>;
179
+ }
180
+
181
+ export { type AlchemyPayBrowserAdapterOptions as A, type OnRampPort as O, type Unsubscribe as U, type OnRampEligibility as a, type OnRampRepository as b, type OnRampProviderId as c, type OnRampOpenParams as d, type OnRampDisallowReason as e, OnRampError as f, type OnRampErrorCode as g, type OnRampSession as h, type OnRampStatus as i, type OnRampStatusEvent as j, AlchemyPayBrowserAdapter as k, normalizeDisallowReason as n };
@@ -0,0 +1,56 @@
1
+ export { k as AlchemyPayBrowserAdapter, A as AlchemyPayBrowserAdapterOptions } from '../../AlchemyPayBrowserAdapter-Dtas3vZE.js';
2
+
3
+ /**
4
+ * AlchemyPay on-ramp URL 빌더 — 순수 함수만.
5
+ *
6
+ * 이 모듈은 DOM/Node API에 의존하지 않으며, secret(HMAC appSecret)도
7
+ * 절대 다루지 않는다. 서명은 백엔드 책임이고, 어댑터는 백엔드가 만들어
8
+ * 준 `signedUrl`을 그대로 사용한다.
9
+ *
10
+ * Sandbox/unsigned 모드는 appId + 파라미터만으로 URL을 조립해
11
+ * AlchemyPay가 위젯 페이지에서 보완 입력을 받게 한다. 프로덕션 흐름은
12
+ * 반드시 `preSignedUrl`을 사용한다.
13
+ */
14
+ declare const ALCHEMY_PAY_RAMP_BASE_URL = "https://ramp.alchemypay.org";
15
+ interface AlchemyPayUrlInput {
16
+ /** AlchemyPay app id (public, sandbox 모드에서만 쓰임). */
17
+ appId?: string;
18
+ /** 사용자 지갑 주소 (필수). */
19
+ address: string;
20
+ /** 도메인 심볼. 어댑터에서 매핑. */
21
+ crypto: string;
22
+ /** 도메인 네트워크 식별자. 어댑터에서 매핑. */
23
+ network: string;
24
+ /** 법정화폐. 미지정 시 보내지 않고 위젯/사용자 선택에 맡긴다. */
25
+ fiat?: string;
26
+ fiatAmount?: number;
27
+ cryptoAmount?: number;
28
+ redirectUrl?: string;
29
+ /**
30
+ * 백엔드가 만들어 준 signed URL. 들어오면 검증 후 그대로 반환 —
31
+ * 어떤 보정도 가하지 않는다.
32
+ */
33
+ preSignedUrl?: string;
34
+ }
35
+ /**
36
+ * URL 빌더 엔트리. preSignedUrl 우선.
37
+ */
38
+ declare function buildAlchemyPayUrl(input: AlchemyPayUrlInput): string;
39
+
40
+ /**
41
+ * 도메인 네트워크/심볼 → AlchemyPay 코드 매핑.
42
+ *
43
+ * AlchemyPay는 자체 네트워크 코드 체계를 쓴다 (예: 'BSC', 'ETH', 'POLYGON').
44
+ * 호출자가 알고 있는 식별자(소문자, EIP-155 chainId 기반 등)를 어댑터 호출
45
+ * 시점에 AlchemyPay 코드로 변환한다.
46
+ *
47
+ * 알 수 없는 값은 그대로 반환 (대문자화만) — 임의 차단보다 provider가
48
+ * 거절하게 두는 게 디버깅에 유리.
49
+ */
50
+ declare function mapNetworkToAlchemyPay(network: string): string;
51
+ /** 도메인 crypto symbol → AlchemyPay crypto code */
52
+ declare function mapCryptoToAlchemyPay(crypto: string): string;
53
+ /** 도메인 fiat → AlchemyPay fiat code */
54
+ declare function mapFiatToAlchemyPay(fiat: string): string;
55
+
56
+ export { ALCHEMY_PAY_RAMP_BASE_URL, type AlchemyPayUrlInput, buildAlchemyPayUrl, mapCryptoToAlchemyPay, mapFiatToAlchemyPay, mapNetworkToAlchemyPay };
@@ -0,0 +1 @@
1
+ import{a,b as m}from"../../chunk-PBPJO43F.js";import{c as e,d as y,e as r,i as o}from"../../chunk-5MH4I7ZN.js";export{a as ALCHEMY_PAY_RAMP_BASE_URL,o as AlchemyPayBrowserAdapter,m as buildAlchemyPayUrl,y as mapCryptoToAlchemyPay,r as mapFiatToAlchemyPay,e as mapNetworkToAlchemyPay};
@@ -0,0 +1 @@
1
+ var p=class extends Error{constructor(e,t,n){super(t),this.name="OnRampError",this.code=e,this.details=n}};function L(i){return["unsupported_country","unsupported_region","sanctioned","kyc_required","provider_outage","unknown"].includes(i)?i:"unknown"}var b={cross:"CROSS","cross-testnet":"CROSS",ethereum:"ETH",eth:"ETH",bsc:"BSC","bnb-chain":"BSC","bsc-testnet":"BSC",polygon:"MATIC",matic:"MATIC",arbitrum:"ARBITRUM",optimism:"OPTIMISM",base:"BASE",1:"ETH",56:"BSC",97:"BSC",137:"MATIC",42161:"ARBITRUM",10:"OPTIMISM",8453:"BASE",612044:"CROSS",612055:"CROSS"};function y(i){let e=i.trim().toLowerCase();return b[e]??i.toUpperCase()}function h(i){return i.trim().toUpperCase()}function D(i){return i.trim().toUpperCase()}var O={dev:"https://dev-embedded-wallet-gateway.crosstoken.io/api/v1",stage:"https://stg-embedded-wallet-gateway.crosstoken.io/api/v1",production:"https://embedded-wallet-gateway.crosstoken.io/api/v1"},g={eligibility:"/onramp/allowed",sign:"/onramp/url"};function f(i){try{return import.meta.env?.[i]}catch{return}}function m(i){if(!(typeof process>"u"))return process.env?.[i]}function v(){switch((f("VITE_CROSSX_ENVIRONMENT")??m("NEXT_PUBLIC_CROSSX_ENVIRONMENT")??m("CROSSX_ENVIRONMENT"))?.toLowerCase()){case"dev":case"development":return"dev";case"stage":case"staging":case"stg":return"stage";default:return"production"}}function R(){let i=f("VITE_CROSSX_ONRAMP_BASE_URL")??m("NEXT_PUBLIC_CROSSX_ONRAMP_BASE_URL"),e=v(),t=i??O[e];return E(t),t}function E(i){let e;try{e=new URL(i)}catch{throw new Error(`[onramp] Invalid base URL: ${i}`)}if(e.protocol==="https:")return;let t=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname.endsWith(".local");if(!(e.protocol==="http:"&&t))throw new Error(`[onramp] base URL must be https (or http://localhost for dev). Got: ${i}`)}var T=5e3,I=1e4,d=class{constructor(e){if(!e.projectId||!e.projectId.trim())throw new Error("[HttpOnRampRepository] projectId is required");this.projectId=e.projectId,this.appId=e.appId,this.appType=e.appType,this.baseUrl=(e.baseUrl??R()).replace(/\/+$/,""),this.paths={eligibility:e.paths?.eligibility??g.eligibility,sign:e.paths?.sign??g.sign},this.eligibilityTimeoutMs=e.eligibilityTimeoutMs??T,this.signTimeoutMs=e.signTimeoutMs??I,this.fallbackProvider=e.fallbackProvider??"alchemypay"}async fetchEligibility(e){let t=this.baseUrl+this.paths.eligibility,n={provider:this.fallbackProvider,allowed:!1,reason:"unknown"},o;try{let r=new AbortController,a=setTimeout(()=>r.abort(),this.eligibilityTimeoutMs),s=await fetch(t,{method:"GET",signal:r.signal,headers:this.buildHeaders({accept:!0}),credentials:"include"});if(clearTimeout(a),!s.ok)return n;o=await s.json()}catch{return n}return this.parseEligibility(o)}async requestSignedUrl(e){let t=this.baseUrl+this.paths.sign,n=new AbortController,o=setTimeout(()=>n.abort(),this.signTimeoutMs);try{let r=e.params,a={address:r.address};r.redirectUrl&&(a.redirect_url=r.redirectUrl),r.crypto&&r.crypto.trim()&&(a.crypto=h(r.crypto)),r.network&&r.network.trim()&&(a.network=y(r.network)),r.fiat&&r.fiat.trim()&&(a.fiat=r.fiat.trim()),typeof r.cryptoAmount=="number"&&r.cryptoAmount>0?a.crypto_amount=r.cryptoAmount:typeof r.fiatAmount=="number"&&r.fiatAmount>0&&(a.fiat_amount=r.fiatAmount);let s=await fetch(t,{method:"POST",signal:n.signal,headers:this.buildHeaders({accept:!0,contentType:!0,idempotency:!0}),credentials:"include",body:JSON.stringify(a)});if(!s.ok)throw new p("SIGN_FAILED",`Sign endpoint returned ${s.status}`);let c=await s.json(),l=c&&typeof c=="object"&&c.data&&typeof c.data=="object"?c.data:c,u=l?l.url:void 0;if(typeof u!="string"||!u)throw new p("SIGN_FAILED","Sign response missing url");return u}finally{clearTimeout(o)}}buildHeaders(e){let t={"X-Project-Id":this.projectId};return e.accept&&(t.Accept="application/json"),e.contentType&&(t["Content-Type"]="application/json"),e.idempotency&&(t["Idempotency-Key"]=P()),this.appId&&(t["X-App-Id"]=this.appId),this.appType&&(t["X-App-Type"]=this.appType),t}parseEligibility(e){if(!e||typeof e!="object")return{provider:this.fallbackProvider,allowed:!1,reason:"unknown"};let t=e,n=t.data&&typeof t.data=="object"?t.data:t,o=n.isAllowed===!0,r={provider:this.fallbackProvider,allowed:o};return typeof n.countCode=="string"&&(r.country=n.countCode),o||(r.reason="unknown"),r}};function P(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`idem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}var S=300*1e3,A="width=480,height=720,popup=yes",_=500,U=300,M=["https://ramp.alchemypay.org","https://ramptest.alchemypay.org","https://ramp-sandbox.alchemypay.org"],w=class{constructor(e){this.provider="alchemypay";this.cache=new Map;this.statusListeners=new Set;this.activeWatch=null;this.cancelGraceTimer=null;if(e.repository)this.repository=e.repository;else{if(!e.projectId||!e.projectId.trim())throw new Error("[AlchemyPayBrowserAdapter] projectId is required when repository is not provided");this.repository=new d({projectId:e.projectId,appId:e.appId,appType:e.appType})}this.eligibilityTtlMs=e.eligibilityTtlMs??S,this.windowFeatures=e.windowFeatures??A,this.trustedOrigins=e.trustedOrigins??M,this.popupPollIntervalMs=e.popupPollIntervalMs??_,this.cancelGraceMs=e.cancelGraceMs??U}async getEligibility(e){let t=`network:${e?.network??""}`,n=this.cache.get(t);if(n&&n.expiresAt>Date.now())return n.value;let o;try{o=await this.repository.fetchEligibility({network:e?.network})}catch{o={provider:this.provider,allowed:!1,reason:"unknown"}}return this.storeCache(t,o),o}storeCache(e,t){let n=typeof t.expiresAt=="number"?t.expiresAt:Date.now()+this.eligibilityTtlMs;this.cache.set(e,{value:t,expiresAt:n})}onStatus(e){return this.statusListeners.add(e),()=>{this.statusListeners.delete(e)}}async close(e){if(this.activeWatch?.sessionId===e){try{this.activeWatch.popup.close()}catch{}this.stopWatching()}}emit(e){for(let t of this.statusListeners)try{t(e)}catch(n){console.error("[onramp] onStatus listener threw:",n)}}startWatching(e,t){this.stopWatching(),console.debug("[onramp] watching popup",{sessionId:e,trustedOrigins:this.trustedOrigins});let n=setInterval(()=>{t.closed&&(clearInterval(n),console.debug("[onramp] popup closed, waiting grace",{sessionId:e,graceMs:this.cancelGraceMs}),this.cancelGraceTimer=setTimeout(()=>{this.cancelGraceTimer=null,console.debug("[onramp] grace expired \u2192 cancelled",{sessionId:e}),this.emit({sessionId:e,status:"cancelled"}),this.stopWatching()},this.cancelGraceMs))},this.popupPollIntervalMs),o=r=>{if(!this.trustedOrigins.includes(r.origin))return;console.debug("[onramp] message from trusted origin",{sessionId:e,origin:r.origin,data:r.data});let a=r.data;if(!a||typeof a!="object")return;let s=String(a.status??a.type??"").toLowerCase(),c=typeof a.txHash=="string"?a.txHash:void 0,l=typeof a.message=="string"?a.message:void 0;/complete|success|paid|pay-success/.test(s)?(console.debug("[onramp] message \u2192 completed",{sessionId:e,status:s,txHash:c}),this.emit({sessionId:e,status:"completed",txHash:c}),this.stopWatching()):/cancel/.test(s)?(console.debug("[onramp] message \u2192 cancelled",{sessionId:e,status:s}),this.emit({sessionId:e,status:"cancelled"}),this.stopWatching()):/fail|error/.test(s)?(console.debug("[onramp] message \u2192 failed",{sessionId:e,status:s,errorMessage:l}),this.emit({sessionId:e,status:"failed",errorMessage:l}),this.stopWatching()):/pending|processing/.test(s)?(console.debug("[onramp] message \u2192 pending",{sessionId:e,status:s}),this.emit({sessionId:e,status:"pending"})):console.debug("[onramp] message ignored (unknown status)",{sessionId:e,rawStatus:s,data:r.data})};window.addEventListener("message",o),this.activeWatch={sessionId:e,popup:t,cleanup:()=>{clearInterval(n),window.removeEventListener("message",o)}}}stopWatching(){this.cancelGraceTimer&&(clearTimeout(this.cancelGraceTimer),this.cancelGraceTimer=null),this.activeWatch&&(this.activeWatch.cleanup(),this.activeWatch=null)}async open(e){if(!e.address)throw new p("MISSING_ADDRESS","address is required");let t;try{t=await this.repository.requestSignedUrl({params:e})}catch(r){throw r instanceof p?r:new p("SIGN_FAILED",r instanceof Error?r.message:"Failed to obtain signed URL",{cause:String(r)})}if(typeof window>"u")throw new p("POPUP_BLOCKED","window is not available (SSR)");let n=window.open(t,"_blank",this.windowFeatures);if(!n)throw new p("POPUP_BLOCKED","Popup window was blocked by the browser");let o=k();return this.startWatching(o,n),{sessionId:o,url:t,provider:this.provider,openedAt:Date.now()}}};function k(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}export{p as a,L as b,y as c,h as d,D as e,g as f,R as g,d as h,w as i};
@@ -0,0 +1 @@
1
+ import{c as o,d as a,e as s}from"./chunk-5MH4I7ZN.js";var n="https://ramp.alchemypay.org";function m(r){if(r.preSignedUrl)return i(r.preSignedUrl,"preSignedUrl");if(!r.appId)throw new Error("[AlchemyPay] appId or preSignedUrl is required to build a URL");if(!r.address)throw new Error("[AlchemyPay] address is required");let t={appId:r.appId,crypto:a(r.crypto),network:o(r.network),address:r.address};r.fiat&&r.fiat.trim()&&(t.fiat=s(r.fiat)),typeof r.cryptoAmount=="number"&&r.cryptoAmount>0?t.cryptoAmount=String(r.cryptoAmount):typeof r.fiatAmount=="number"&&r.fiatAmount>0&&(t.fiatAmount=String(r.fiatAmount)),r.redirectUrl&&(t.redirectUrl=r.redirectUrl);let e=new URLSearchParams(t).toString();return`${n}/?${e}`}function i(r,t){try{if(new URL(r).protocol!=="https:")throw new Error(`${t} must use https`);return r}catch{throw new Error(`${t} is not a valid URL: ${r}`)}}export{n as a,m as b};
@@ -0,0 +1 @@
1
+ import{a as n,i as m}from"./chunk-5MH4I7ZN.js";var a=class{constructor(t,i,s){this.port=t;this.walletState=i;this.networks=s}async execute(t){let i=t.address??this.walletState.getAddress();if(!i)throw new n("MISSING_ADDRESS","Wallet address is not available");if(!t.network||!t.network.trim())throw new n("UNSUPPORTED_NETWORK","Network identifier is empty");if(!t.crypto||!t.crypto.trim())throw new n("UNSUPPORTED_CRYPTO","Crypto symbol is empty");if(!this.networks.findByIdentifier(t.network))throw new n("UNSUPPORTED_NETWORK",`Unknown network "${t.network}"`,{network:t.network});let o=typeof t.fiatAmount=="number"&&t.fiatAmount>0,p=typeof t.cryptoAmount=="number"&&t.cryptoAmount>0;if(t.fiatAmount!==void 0&&!o)throw new n("INVALID_AMOUNT","fiatAmount must be a positive number");if(t.cryptoAmount!==void 0&&!p)throw new n("INVALID_AMOUNT","cryptoAmount must be a positive number");let e=await this.port.getEligibility({network:t.network});if(!e.allowed)throw new n("PROVIDER_DISABLED",e.message??"On-ramp is not available in this region",{reason:e.reason,country:e.country});return this.port.open({...t,address:i})}};var O={findByIdentifier(r){if(!(!r||!r.trim()))return{id:r,name:r}}},R={getAddress:()=>{}};function f(r){let{walletState:t=R,networks:i=O,...s}=r,o=new m(s),p=new a(o,t,i);return{port:o,getEligibility:e=>o.getEligibility(e),open:e=>p.execute(e),onStatus:e=>o.onStatus?o.onStatus(e):(()=>{})}}export{a,f as b};
@@ -0,0 +1,79 @@
1
+ import { O as OnRampPort, d as OnRampOpenParams, h as OnRampSession, A as AlchemyPayBrowserAdapterOptions, a as OnRampEligibility, j as OnRampStatusEvent, U as Unsubscribe } from './AlchemyPayBrowserAdapter-Dtas3vZE.js';
2
+
3
+ /**
4
+ * 지갑 주소 / 네트워크 / 금액을 검증하고 eligibility 재확인 후 결제 위젯을 연다.
5
+ *
6
+ * react 훅이 마운트 시점에 1회 eligibility를 캐시해 두지만, 사용자가
7
+ * 그 사이에 다른 네트워크로 전환했거나 캐시 만료 직전에 클릭할 수 있어
8
+ * 호출 직전 1회 더 확인한다. 어댑터의 캐시 hit이면 추가 네트워크 비용은 0.
9
+ */
10
+ interface WalletStateReader {
11
+ /** 현재 연결된 지갑 주소 (없으면 undefined). */
12
+ getAddress(): string | undefined;
13
+ }
14
+ /**
15
+ * 네트워크 식별자 검증용 최소 인터페이스. core(connect-kit) `NetworkConfig`에
16
+ * 의존하지 않도록 onramp가 알아야 할 최소 모양만 노출 — connect-kit이 어댑터로
17
+ * 변환해서 주입한다.
18
+ */
19
+ interface OnRampNetworkInfo {
20
+ id: number | string;
21
+ name: string;
22
+ }
23
+ interface NetworkRegistry {
24
+ /** core network identifier로 NetworkInfo 조회. 모르면 undefined. */
25
+ findByIdentifier(identifier: string): OnRampNetworkInfo | undefined;
26
+ }
27
+ declare class OpenOnRampUseCase {
28
+ private readonly port;
29
+ private readonly walletState;
30
+ private readonly networks;
31
+ constructor(port: OnRampPort, walletState: WalletStateReader, networks: NetworkRegistry);
32
+ execute(params: OnRampOpenParams): Promise<OnRampSession>;
33
+ }
34
+
35
+ /**
36
+ * 프레임워크 무관 함수형 진입점.
37
+ *
38
+ * React/Vue/Svelte/vanilla 어디서든:
39
+ * const onramp = createOnRamp({ projectId: '<crossProjectId>' });
40
+ * const e = await onramp.getEligibility({ network: 'cross' });
41
+ * if (e.allowed) await onramp.open({ address, crypto, network, fiatAmount });
42
+ *
43
+ * 내부적으로 OpenOnRampUseCase + AlchemyPayBrowserAdapter 조립. 호출자가
44
+ * walletState/networkRegistry를 명시적으로 주입하지 않은 경우(대부분의
45
+ * 사용 케이스), open(params)에 받은 address/network를 그대로 사용한다.
46
+ */
47
+
48
+ interface CreateOnRampOptions extends AlchemyPayBrowserAdapterOptions {
49
+ /**
50
+ * 지갑 주소 reader. 미지정 시 open(params)의 address를 그대로 사용.
51
+ * connect-kit-react가 자동 mount할 때 wagmi useAccount를 어댑터한 reader를 주입한다.
52
+ */
53
+ walletState?: WalletStateReader;
54
+ /**
55
+ * 네트워크 식별자 검증용. 미지정 시 어떤 식별자도 통과시키는 permissive registry 사용
56
+ * (provider가 모르는 network 코드면 백엔드/AlchemyPay가 거절).
57
+ */
58
+ networks?: NetworkRegistry;
59
+ }
60
+ interface OnRamp {
61
+ /** 내부 어댑터. 고급 사용자가 직접 다뤄야 할 때 노출. */
62
+ readonly port: OnRampPort;
63
+ getEligibility(ctx?: {
64
+ network?: string;
65
+ }): Promise<OnRampEligibility>;
66
+ open(params: OnRampOpenParams | (Omit<OnRampOpenParams, 'address'> & {
67
+ address?: string;
68
+ })): Promise<OnRampSession>;
69
+ /**
70
+ * popup의 결제 상태 이벤트 구독.
71
+ * - cancelled: popup이 닫혔을 때 (의도적 취소 보장 X)
72
+ * - completed/failed/pending: AlchemyPay가 postMessage를 보낼 때만
73
+ * 진실의 단일 소스는 webhook이라는 점을 잊지 말 것.
74
+ */
75
+ onStatus(cb: (e: OnRampStatusEvent) => void): Unsubscribe;
76
+ }
77
+ declare function createOnRamp(options: CreateOnRampOptions): OnRamp;
78
+
79
+ export { type CreateOnRampOptions as C, type NetworkRegistry as N, type OnRamp as O, type WalletStateReader as W, type OnRampNetworkInfo as a, OpenOnRampUseCase as b, createOnRamp as c };
@@ -0,0 +1,149 @@
1
+ import { O as OnRampPort, a as OnRampEligibility, b as OnRampRepository, c as OnRampProviderId, d as OnRampOpenParams } from './AlchemyPayBrowserAdapter-Dtas3vZE.js';
2
+ export { e as OnRampDisallowReason, f as OnRampError, g as OnRampErrorCode, h as OnRampSession, i as OnRampStatus, j as OnRampStatusEvent, U as Unsubscribe, n as normalizeDisallowReason } from './AlchemyPayBrowserAdapter-Dtas3vZE.js';
3
+ export { C as CreateOnRampOptions, N as NetworkRegistry, O as OnRamp, a as OnRampNetworkInfo, b as OpenOnRampUseCase, W as WalletStateReader, c as createOnRamp } from './createOnRamp-DyJoY_CH.js';
4
+
5
+ /**
6
+ * Port 위임 + 향후 멀티 프로바이더 머지 로직이 들어갈 자리.
7
+ * 현 1차에서는 얇은 래퍼지만, react 훅이 use case에 의존하도록 두면
8
+ * 추후 확장 무중단.
9
+ */
10
+ declare class GetOnRampEligibilityUseCase {
11
+ private readonly port;
12
+ constructor(port: OnRampPort);
13
+ execute(ctx?: {
14
+ network?: string;
15
+ }): Promise<OnRampEligibility>;
16
+ }
17
+
18
+ /**
19
+ * On-ramp 백엔드 엔드포인트 — 패키지 내부 상수. DApp 개발자는 이 파일을
20
+ * 보지도, 수정할 일도 없다. 환경별 base URL은 빌드 시 환경변수로 override
21
+ * 가능 — Nexus 백엔드가 실제 배포되기 전까지는 examples의 mock 서버 URL을
22
+ * 이 환경변수로 가리킨다.
23
+ *
24
+ * 우선순위 (override):
25
+ * 1) `VITE_CROSSX_ONRAMP_BASE_URL` (Vite 빌드)
26
+ * 2) `NEXT_PUBLIC_CROSSX_ONRAMP_BASE_URL` (Next.js)
27
+ * 3) 환경 식별(`VITE_CROSSX_ENVIRONMENT` / `NEXT_PUBLIC_CROSSX_ENVIRONMENT`)
28
+ * 에 따라 DEFAULT_BASE_URL의 dev/stage/production
29
+ */
30
+ type OnRampEnvironment = 'dev' | 'stage' | 'production';
31
+ interface OnRampEndpointPaths {
32
+ /** Country 기반 on-ramp 허용 여부 — GET, 응답 { isAllowed, countCode } */
33
+ eligibility: string;
34
+ /** cross-pay 주문 생성 + signed URL 반환 — POST, 응답 { url } */
35
+ sign: string;
36
+ }
37
+ declare const DEFAULT_ONRAMP_PATHS: OnRampEndpointPaths;
38
+ declare function getOnRampBaseUrl(): string;
39
+
40
+ interface HttpOnRampRepositoryOptions {
41
+ /** kitConfig.crossProjectId — `X-Project-Id` 헤더로 사용. 필수. */
42
+ projectId: string;
43
+ /** native SDK 흐름에서만 사용. 웹은 생략. */
44
+ appId?: string;
45
+ /** `X-App-Id`와 함께 전송. 'android' | 'ios' | 'windows'. 웹은 생략. */
46
+ appType?: string;
47
+ /** override 안 하면 getOnRampBaseUrl() 사용. */
48
+ baseUrl?: string;
49
+ paths?: Partial<OnRampEndpointPaths>;
50
+ eligibilityTimeoutMs?: number;
51
+ signTimeoutMs?: number;
52
+ fallbackProvider?: OnRampProviderId;
53
+ }
54
+ /**
55
+ * crossx 2.0 embedded-wallet-gateway 호출 리포지토리.
56
+ *
57
+ * 실제 endpoint 스펙 (embedded-wallet-gateway swagger 기준):
58
+ * - GET /onramp/allowed → { isAllowed: boolean, countCode: string }
59
+ * - POST /onramp/url → { url: string }
60
+ * body: { address, redirect_url?, crypto?, network?, fiat?,
61
+ * fiat_amount?, crypto_amount? }
62
+ * 헤더: X-Project-Id, Idempotency-Key (필수), X-App-Id/X-App-Type (선택)
63
+ *
64
+ * crypto/network를 백엔드로 전달해 백엔드가 서명 URL에 그 코인/네트워크를
65
+ * 포함하도록 한다 (그래야 AlchemyPay 위젯에서 해당 코인이 미리 선택된다).
66
+ * AlchemyPay의 `sign`은 파라미터 집합에 대한 HMAC이라 프론트가 서명 후
67
+ * 파라미터를 덧붙일 수 없으므로, 서명 전 단계인 백엔드가 받아야 한다.
68
+ * 호출자가 명시적으로 준 값만 전송 — 미지정 필드는 백엔드 기본값에 맡긴다.
69
+ *
70
+ * 모든 실패는 fail-closed: eligibility는 `{ allowed: false, reason: 'unknown' }`,
71
+ * sign은 OnRampError 던짐.
72
+ *
73
+ * 로컬 개발/데모에서는 외부 프로세스 없이 동작하는 MockOnRampRepository를
74
+ * 대신 주입할 수 있다 (`@nexus-cross/onramp` export).
75
+ */
76
+ declare class HttpOnRampRepository implements OnRampRepository {
77
+ private readonly projectId;
78
+ private readonly appId?;
79
+ private readonly appType?;
80
+ private readonly baseUrl;
81
+ private readonly paths;
82
+ private readonly eligibilityTimeoutMs;
83
+ private readonly signTimeoutMs;
84
+ private readonly fallbackProvider;
85
+ constructor(opts: HttpOnRampRepositoryOptions);
86
+ fetchEligibility(_ctx: {
87
+ network?: string;
88
+ }): Promise<OnRampEligibility>;
89
+ requestSignedUrl(payload: {
90
+ params: OnRampOpenParams;
91
+ }): Promise<string>;
92
+ private buildHeaders;
93
+ private parseEligibility;
94
+ }
95
+
96
+ /**
97
+ * In-memory + Web Crypto HMAC 기반 리포지토리.
98
+ *
99
+ * 외부 프로세스(mock 서버 등) 없이 어댑터를 동작시키기 위한 데모/테스트 전용.
100
+ * - eligibility: 생성 시점에 박은 응답 그대로 반환 (또는 함수로 동적 결정)
101
+ * - sign: appSecret + appId가 주입된 경우 브라우저에서 HMAC-SHA256으로
102
+ * signedUrl 생성. AlchemyPay 공식 알고리즘과 동일.
103
+ *
104
+ * ⚠️ 보안 경고
105
+ * - 이 리포지토리를 production 번들에 포함하면 `appSecret`이 클라이언트로
106
+ * 노출된다. 데모/개발/CI 환경에서만 사용할 것.
107
+ * - 콘솔에 한 번 경고 메시지가 출력된다.
108
+ */
109
+ interface MockOnRampRepositoryOptions {
110
+ /** sandbox/dev 자격증명. secret이 있으면 브라우저에서 HMAC 생성. */
111
+ appId?: string;
112
+ appSecret?: string;
113
+ /** provider 라벨. 기본 'alchemypay'. */
114
+ provider?: OnRampProviderId;
115
+ /**
116
+ * 정적 eligibility 응답. 또는 함수로 ctx 기반 동적 결정.
117
+ * 미지정 시 기본값: { allowed: true, country: 'KR' }
118
+ */
119
+ eligibility?: Partial<OnRampEligibility> | ((ctx: {
120
+ network?: string;
121
+ }) => Partial<OnRampEligibility>);
122
+ /**
123
+ * sign 모드.
124
+ * - 'hmac' (default if appSecret): 진짜 HMAC. 위젯이 sandbox에서 정상 동작.
125
+ * - 'unsigned': URLSearchParams만으로 unsigned URL. AlchemyPay가 거절할 수 있음.
126
+ * - 'log-only': window.open 호출 안 하고 console.log + dummy URL 반환. 흐름만 확인.
127
+ *
128
+ * `appSecret` 없으면 'hmac'은 폴백 'unsigned'.
129
+ */
130
+ signMode?: 'hmac' | 'unsigned' | 'log-only';
131
+ }
132
+ declare class MockOnRampRepository implements OnRampRepository {
133
+ private readonly appId?;
134
+ private readonly appSecret?;
135
+ private readonly provider;
136
+ private readonly eligibilityFn;
137
+ private readonly signMode;
138
+ constructor(opts?: MockOnRampRepositoryOptions);
139
+ private warnOnce;
140
+ fetchEligibility(ctx: {
141
+ network?: string;
142
+ }): Promise<OnRampEligibility>;
143
+ requestSignedUrl(payload: {
144
+ params: OnRampOpenParams;
145
+ }): Promise<string>;
146
+ private buildAlchemyPayParams;
147
+ }
148
+
149
+ export { DEFAULT_ONRAMP_PATHS, GetOnRampEligibilityUseCase, HttpOnRampRepository, type HttpOnRampRepositoryOptions, MockOnRampRepository, type MockOnRampRepositoryOptions, OnRampEligibility, type OnRampEndpointPaths, type OnRampEnvironment, OnRampOpenParams, OnRampPort, OnRampProviderId, OnRampRepository, getOnRampBaseUrl };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{a as u,b as P}from"./chunk-U66BKMXR.js";import{a as s}from"./chunk-PBPJO43F.js";import{a as i,b as O,c as d,d as g,e as R,f as b,g as h,h as w}from"./chunk-5MH4I7ZN.js";var l=class{constructor(e){this.port=e}execute(e){return this.port.getEligibility(e)}};var f="__crossx_mock_onramp_warning__",c=class{constructor(e={}){this.appId=e.appId,this.appSecret=e.appSecret,this.provider=e.provider??"alchemypay";let t=e.eligibility;this.eligibilityFn=typeof t=="function"?t:()=>t??{allowed:!0,country:"KR"};let o=e.signMode??(e.appSecret?"hmac":"unsigned");this.signMode=o==="hmac"&&!e.appSecret?"unsigned":o,this.warnOnce()}warnOnce(){if(typeof globalThis>"u")return;let e=globalThis;e[f]||(e[f]=!0,console.warn("[onramp] MockOnRampRepository active \u2014 appSecret may be exposed in this bundle. Use only for dev/demo, never in production."))}async fetchEligibility(e){let t=this.eligibilityFn(e);return{provider:t.provider??this.provider,allowed:t.allowed??!0,country:t.country,reason:t.allowed===!1?t.reason??"unknown":t.reason,message:t.message,expiresAt:t.expiresAt}}async requestSignedUrl(e){let{params:t}=e;if(!t.address)throw new i("MISSING_ADDRESS","address is required");if(this.signMode==="log-only"){let r=`${s}/?mock=log-only&address=${encodeURIComponent(t.address)}`;return console.log("[onramp:mock] log-only sign \u2014 would open:",r),r}let o=this.buildAlchemyPayParams(t),n=Object.keys(o).sort().map(r=>`${r}=${encodeURIComponent(o[r])}`).join("&");if(this.signMode==="unsigned")return`${s}/?${n}`;let a=await A(this.appSecret,n);return`${s}/?${n}&sign=${encodeURIComponent(a)}`}buildAlchemyPayParams(e){let t={crypto:g(e.crypto),network:d(e.network),address:e.address,timestamp:String(Date.now())};return this.appId&&(t.appId=this.appId),e.fiat&&e.fiat.trim()&&(t.fiat=R(e.fiat)),typeof e.cryptoAmount=="number"&&e.cryptoAmount>0?t.cryptoAmount=String(e.cryptoAmount):typeof e.fiatAmount=="number"&&e.fiatAmount>0&&(t.fiatAmount=String(e.fiatAmount)),typeof e.redirectUrl=="string"&&e.redirectUrl&&(t.redirectUrl=e.redirectUrl),t}};async function A(p,e){if(typeof crypto>"u"||!crypto.subtle)throw new i("SIGN_FAILED","Web Crypto (crypto.subtle) not available \u2014 cannot HMAC in this environment");let t=new TextEncoder,o=await crypto.subtle.importKey("raw",t.encode(p),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),n=await crypto.subtle.sign("HMAC",o,t.encode(e)),a=new Uint8Array(n),r="";for(let m=0;m<a.length;m++)r+=String.fromCharCode(a[m]);if(typeof btoa=="function")return btoa(r);let y=globalThis.Buffer;if(y)return y.from(r,"binary").toString("base64");throw new i("SIGN_FAILED","No base64 encoder available")}export{b as DEFAULT_ONRAMP_PATHS,l as GetOnRampEligibilityUseCase,w as HttpOnRampRepository,c as MockOnRampRepository,i as OnRampError,u as OpenOnRampUseCase,P as createOnRamp,h as getOnRampBaseUrl,O as normalizeDisallowReason};
@@ -0,0 +1,118 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { C as CreateOnRampOptions, O as OnRamp } from '../createOnRamp-DyJoY_CH.js';
4
+ import { a as OnRampEligibility, j as OnRampStatusEvent, d as OnRampOpenParams, h as OnRampSession, f as OnRampError } from '../AlchemyPayBrowserAdapter-Dtas3vZE.js';
5
+ export { e as OnRampDisallowReason, g as OnRampErrorCode, c as OnRampProviderId, i as OnRampStatus } from '../AlchemyPayBrowserAdapter-Dtas3vZE.js';
6
+
7
+ interface OnRampProviderProps {
8
+ /** createOnRamp options. 가장 흔한 케이스는 { projectId } 한 줄. */
9
+ config: CreateOnRampOptions;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * On-ramp Provider — 어떤 React 앱에든 단독으로 마운트 가능.
14
+ * connect-kit-react를 안 쓰는 DApp도 이걸 직접 마운트해서 `useOnRamp`/
15
+ * `useOnRampEligibility`를 쓸 수 있다.
16
+ *
17
+ * connect-kit-react는 `kitConfig.onRampEnabled === true`일 때 이 Provider를
18
+ * 내부에서 자동 mount하며 `kitConfig.crossProjectId`를 projectId로 사용한다.
19
+ */
20
+ declare function OnRampProvider({ config, children }: OnRampProviderProps): react_jsx_runtime.JSX.Element;
21
+
22
+ interface UseOnRampEligibilityResult {
23
+ /** Provider mount 여부 (= 항상 true. 컨벤션 통일 위해 노출). */
24
+ isAvailable: boolean;
25
+ /** 조회 결과. 마운트 직후엔 undefined. */
26
+ eligibility: OnRampEligibility | undefined;
27
+ /** 조회 중 또는 재시도 대기 중 여부. retry 도중에도 true 유지. */
28
+ isLoading: boolean;
29
+ /** 강제 재조회 (retry 카운터 초기화). */
30
+ refresh: () => Promise<void>;
31
+ }
32
+ interface UseOnRampEligibilityOptions {
33
+ /** 네트워크 컨텍스트. 변경 시 자동 재조회. */
34
+ network?: string;
35
+ /**
36
+ * fail-closed(allowed=false + reason='unknown') 응답을 받으면 점진적 backoff로
37
+ * 재시도. 토큰 발급 타이밍이 늦어 401/-10002로 떨어지는 케이스에서 access token이
38
+ * 잡힌 뒤 자동 복구되도록 한다. allowed=true 또는 명시적 reason(예:
39
+ * unsupported_country)을 받으면 즉시 중단. 기본 활성.
40
+ */
41
+ retry?: {
42
+ /** 최대 시도 횟수 (첫 시도 포함). 기본 8. */
43
+ maxAttempts?: number;
44
+ /** 첫 backoff 지연(ms). 기본 500. */
45
+ initialDelayMs?: number;
46
+ /** backoff 상한(ms). 기본 5000. */
47
+ maxDelayMs?: number;
48
+ /** 배수. 기본 1.8. */
49
+ factor?: number;
50
+ /** false면 retry 비활성. */
51
+ enabled?: boolean;
52
+ };
53
+ }
54
+ /**
55
+ * eligibility만 필요한 호출자(배너, 안내 등)가 `useOnRamp`의 무거운
56
+ * 표면(open, isLoading 등) 없이 쓸 수 있는 가벼운 훅.
57
+ *
58
+ * 토큰 발급 타이밍 차로 첫 호출이 -10002(unauthorized) → 어댑터 fail-closed로
59
+ * `{ allowed: false, reason: 'unknown' }`을 받는 케이스가 있어 점진적 backoff로
60
+ * 자동 재시도한다. `allowed: true` 또는 다른 reason을 받으면 즉시 중단.
61
+ */
62
+ declare function useOnRampEligibility(opts?: UseOnRampEligibilityOptions): UseOnRampEligibilityResult;
63
+
64
+ interface UseOnRampResult extends UseOnRampEligibilityResult {
65
+ /** open(params): 검증 + eligibility 재확인 + 위젯 오픈 일체. */
66
+ open: (params: OnRampOpenParams | (Omit<OnRampOpenParams, 'address'> & {
67
+ address?: string;
68
+ })) => Promise<OnRampSession>;
69
+ /** open 진행 중 여부. (eligibility의 isLoading과는 별개) */
70
+ isOpening: boolean;
71
+ /** 마지막 open() 에러. 새 호출이 들어오면 null로 리셋. */
72
+ error: OnRampError | null;
73
+ /** 마지막 성공 세션. */
74
+ lastSession: OnRampSession | null;
75
+ /** 마지막으로 받은 status 이벤트 (옵션). pending → completed/failed/cancelled 흐름. */
76
+ lastStatus: OnRampStatusEvent | null;
77
+ }
78
+ interface UseOnRampOptions {
79
+ /** eligibility 조회용 네트워크. */
80
+ network?: string;
81
+ /**
82
+ * popup 결제 상태 이벤트 콜백.
83
+ * - cancelled: popup이 닫혔을 때 (의도적 취소 보장 X)
84
+ * - completed/failed/pending: AlchemyPay가 postMessage를 보낼 때만
85
+ * 진실의 단일 소스는 webhook(서버) — 여기 이벤트는 UX 보조용.
86
+ */
87
+ onStatus?: (e: OnRampStatusEvent) => void;
88
+ }
89
+ /**
90
+ * On-ramp 메인 훅. address는 Provider 시점에 주입된 walletState reader에서
91
+ * 자동 풀린다 (예: connect-kit-react가 wagmi useAccount를 어댑터해 줌).
92
+ * walletState가 미주입이면 open(params)의 address 필수.
93
+ */
94
+ declare function useOnRamp(opts?: UseOnRampOptions): UseOnRampResult;
95
+
96
+ /**
97
+ * OnRampProvider 안에서는 OnRamp facade를 반환, 밖에서는 null.
98
+ *
99
+ * `useOnRamp`는 conditionally 호출이 어려운(React 룰) 컴포넌트가
100
+ * "있으면 쓰고, 없으면 비활성"으로 동작하고 싶을 때 사용한다.
101
+ * 예: connect-kit-react ConnectButton이 `onRampId` 미설정 환경에서도
102
+ * 깨지지 않아야 하지만, 설정되어 있으면 Buy 버튼을 자동으로 연결.
103
+ */
104
+ declare function useOptionalOnRamp(): OnRamp | null;
105
+
106
+ /**
107
+ * `useOnRampEligibility`의 optional 변형.
108
+ *
109
+ * `OnRampProvider`가 mount되지 않은 환경에서도 안전하게 사용할 수 있다
110
+ * (provider 미마운트 시 `isAvailable: false` + `eligibility: undefined`).
111
+ *
112
+ * connect-kit-react의 `ConnectButton`처럼 onRamp 활성/비활성 모두 지원해야
113
+ * 하는 컴포넌트에서 사용. provider가 있으면 `useOnRampEligibility`와 동일한
114
+ * backoff retry 동작을 한다.
115
+ */
116
+ declare function useOptionalOnRampEligibility(opts?: UseOnRampEligibilityOptions): UseOnRampEligibilityResult;
117
+
118
+ export { OnRampEligibility, OnRampError, OnRampOpenParams, OnRampProvider, type OnRampProviderProps, OnRampSession, OnRampStatusEvent, type UseOnRampEligibilityOptions, type UseOnRampEligibilityResult, type UseOnRampOptions, type UseOnRampResult, useOnRamp, useOnRampEligibility, useOptionalOnRamp, useOptionalOnRampEligibility };
@@ -0,0 +1 @@
1
+ import{b as h}from"../chunk-U66BKMXR.js";import{a as g}from"../chunk-5MH4I7ZN.js";import{useMemo as I}from"react";import{createContext as T,useContext as V}from"react";var O=T(null);function w(){let e=V(O);if(!e)throw new Error("useOnRamp must be used within <OnRampProvider>");return e}import{jsx as F}from"react/jsx-runtime";function _({config:e,children:i}){let a=I(()=>({onramp:h(e)}),[e.projectId,e.appId,e.appType,e.repository,e.eligibilityTtlMs,e.windowFeatures,e.walletState,e.networks]);return F(O.Provider,{value:a,children:i})}import{useCallback as W,useEffect as Y,useRef as B,useState as v}from"react";import{useCallback as k,useEffect as N,useRef as P,useState as U}from"react";var x={maxAttempts:12,initialDelayMs:500,maxDelayMs:3e3,factor:1.5,enabled:!0};function M(e={}){let{onramp:i}=w(),[a,f]=U(void 0),[c,d]=U(!0),s={maxAttempts:e.retry?.maxAttempts??x.maxAttempts,initialDelayMs:e.retry?.initialDelayMs??x.initialDelayMs,maxDelayMs:e.retry?.maxDelayMs??x.maxDelayMs,factor:e.retry?.factor??x.factor,enabled:e.retry?.enabled??x.enabled},R=P(s);R.current=s;let o=P(null),m=p=>!!p&&p.allowed===!1&&p.reason==="unknown",r=k(async p=>{o.current&&(o.current.cancelled=!0);let n={cancelled:!1};o.current=n,d(!0);let t=R.current,u=0,y=t.initialDelayMs;for(;!n.cancelled;){u+=1;let l;try{l=await i.getEligibility({network:p})}catch{l={provider:"alchemypay",allowed:!1,reason:"unknown"}}if(n.cancelled)return;if(!(t.enabled&&m(l)&&u<t.maxAttempts)){f(l),d(!1);return}await new Promise(C=>{let D=setTimeout(C,y)}),y=Math.min(Math.round(y*t.factor),t.maxDelayMs)}},[i]),b=k(async()=>{await r(e.network)},[r,e.network]);return N(()=>(r(e.network),()=>{o.current&&(o.current.cancelled=!0)}),[r,e.network]),{isAvailable:!0,eligibility:a,isLoading:c,refresh:b}}function H(e={}){let{onramp:i}=w(),a=M({network:e.network}),[f,c]=v(!1),[d,s]=v(null),[R,o]=v(null),[m,r]=v(null),b=B(e.onStatus);b.current=e.onStatus,Y(()=>i.onStatus(n=>{r(n),b.current?.(n)}),[i]);let p=W(async n=>{s(null),c(!0);try{let t=await i.open(n);return o(t),t}catch(t){let u=t instanceof g?t:new g("PROVIDER_DISABLED",t instanceof Error?t.message:"On-ramp failed");throw s(u),u}finally{c(!1)}},[i]);return{...a,open:p,isOpening:f,error:d,lastSession:R,lastStatus:m}}import{useContext as q}from"react";function z(){return q(O)?.onramp??null}import{useCallback as A,useContext as G,useEffect as J,useRef as L,useState as j}from"react";var E={maxAttempts:12,initialDelayMs:500,maxDelayMs:3e3,factor:1.5,enabled:!0};function K(e={}){let a=G(O)?.onramp??null,[f,c]=j(void 0),[d,s]=j(!!a),R={maxAttempts:e.retry?.maxAttempts??E.maxAttempts,initialDelayMs:e.retry?.initialDelayMs??E.initialDelayMs,maxDelayMs:e.retry?.maxDelayMs??E.maxDelayMs,factor:e.retry?.factor??E.factor,enabled:e.retry?.enabled??E.enabled},o=L(R);o.current=R;let m=L(null),r=A(async p=>{if(!a){c(void 0),s(!1);return}m.current&&(m.current.cancelled=!0);let n={cancelled:!1};m.current=n,s(!0);let t=o.current,u=0,y=t.initialDelayMs;for(;!n.cancelled;){u+=1;let l;try{l=await a.getEligibility({network:p})}catch{l={provider:"alchemypay",allowed:!1,reason:"unknown"}}if(n.cancelled)return;let S=l.allowed===!1&&l.reason==="unknown";if(!(t.enabled&&S&&u<t.maxAttempts)){c(l),s(!1);return}await new Promise(D=>setTimeout(D,y)),y=Math.min(Math.round(y*t.factor),t.maxDelayMs)}},[a]),b=A(async()=>{await r(e.network)},[r,e.network]);return J(()=>(r(e.network),()=>{m.current&&(m.current.cancelled=!0)}),[r,e.network]),{isAvailable:!!a,eligibility:f,isLoading:d,refresh:b}}export{g as OnRampError,_ as OnRampProvider,H as useOnRamp,M as useOnRampEligibility,z as useOptionalOnRamp,K as useOptionalOnRampEligibility};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nexus-cross/onramp",
3
+ "version": "1.3.0",
4
+ "description": "fiat→crypto on-ramp integration (AlchemyPay first). Framework-agnostic core + React adapter.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./react": {
14
+ "types": "./dist/react/index.d.ts",
15
+ "import": "./dist/react/index.js"
16
+ },
17
+ "./adapters/alchemypay": {
18
+ "types": "./dist/adapters/alchemypay/index.d.ts",
19
+ "import": "./dist/adapters/alchemypay/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "publishConfig": {
27
+ "registry": "https://registry.npmjs.org",
28
+ "access": "public"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^18.0.0 || ^19.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "react": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@types/react": "^19.0.0",
40
+ "react": "^19.0.0",
41
+ "tsup": "^8.4.0",
42
+ "typescript": "^5.7.0"
43
+ },
44
+ "license": "MIT",
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "test": "echo 'no tests yet'",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }