odin-connect 1.3.3 → 1.4.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.
package/dist/index.d.ts CHANGED
@@ -194,7 +194,7 @@ declare class OdinApiClient {
194
194
  followers: number;
195
195
  following: number;
196
196
  }>;
197
- set apiKey(key: string);
197
+ set apiKey(key: string | null);
198
198
  get apiKey(): string | null;
199
199
  }
200
200
 
@@ -362,6 +362,7 @@ interface AppInitOptions {
362
362
  name: string;
363
363
  icon?: string;
364
364
  env?: Environment;
365
+ slug?: string;
365
366
  }
366
367
  interface BaseConnectOptions {
367
368
  open?: WindowClientSettings;
@@ -382,25 +383,40 @@ declare class Connect {
382
383
  private _api;
383
384
  private _window;
384
385
  private _odin;
386
+ private _storage;
385
387
  constructor(appInfo?: Partial<AppInitOptions>);
386
388
  private createUrl;
387
389
  get origin(): string;
388
390
  get appInfo(): AppInitOptions | null;
391
+ get slug(): string;
389
392
  connect({ open, requires_api, requires_delegation, targets, }?: ConnectOptions | undefined): Promise<ConnectedUser>;
390
393
  get api(): OdinApiClient;
391
394
  get odin(): OdinCanisterClient;
392
395
  get currentEnv(): "prod" | "dev" | "local" | "_preview";
396
+ restoreSession(): ConnectedUser | null;
397
+ disconnect(): void;
398
+ isSessionValid(): boolean;
393
399
  hello(): void;
394
400
  }
395
401
 
402
+ declare function isDelegationValid(chainJson: string): boolean;
403
+
396
404
  declare function convertToOdinAmount(numberStr: string | number, token?: Pick<BaseToken, "decimals" | "divisibility">): bigint;
397
405
  type TokenValue = string | File | null | bigint;
398
406
  declare const createTokenValidators: Partial<Record<keyof Token, (value: TokenValue) => string | undefined>>;
399
407
 
400
408
  declare const index_convertToOdinAmount: typeof convertToOdinAmount;
401
409
  declare const index_createTokenValidators: typeof createTokenValidators;
410
+ declare const index_isDelegationValid: typeof isDelegationValid;
402
411
  declare namespace index {
403
- export { index_convertToOdinAmount as convertToOdinAmount, index_createTokenValidators as createTokenValidators };
412
+ export { index_convertToOdinAmount as convertToOdinAmount, index_createTokenValidators as createTokenValidators, index_isDelegationValid as isDelegationValid };
413
+ }
414
+
415
+ interface SessionData {
416
+ principal: string;
417
+ sessionKey: string;
418
+ delegationChain: string | null;
419
+ jwt: string | null;
404
420
  }
405
421
 
406
- export { type Achievement as OdinAchievement, type AchievementCategory as OdinAchievementCategory, type Activity as OdinActivity, type Balance as OdinBalance, type BaseToken as OdinBaseToken, Connect as OdinConnect, ConnectedUser as OdinConnectedUser, type Token as OdinToken, type TokenWithBalance as OdinTokenWithBalance, type Transaction as OdinTransaction, type User as OdinUser, index as OdinUtils };
422
+ export { type Achievement as OdinAchievement, type AchievementCategory as OdinAchievementCategory, type Activity as OdinActivity, type Balance as OdinBalance, type BaseToken as OdinBaseToken, Connect as OdinConnect, ConnectedUser as OdinConnectedUser, type Token as OdinToken, type TokenWithBalance as OdinTokenWithBalance, type Transaction as OdinTransaction, type User as OdinUser, index as OdinUtils, type SessionData };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import U from'axios';import E from'@apimatic/json-bigint';import {Ed25519KeyIdentity,DelegationIdentity,DelegationChain}from'@dfinity/identity';var C=Object.defineProperty;var R=(n,e)=>{for(var t in e)C(n,t,{get:e[t],enumerable:true});};var x=n=>{try{return E.parse(n)}catch(e){return console.error("Error parsing response:",e),n}},u=class{async get(e,t){return this.fetch(e,{method:"GET",...t})}async fetch(e,t){let i={transformResponse:[x],...t};return (await U(e,i)).data}async post(e,t,i){return this.fetch(e,{method:"POST",data:t,...i})}async put(e,t,i){return this.fetch(e,{method:"PUT",data:t,...i})}async delete(e,t){return this.fetch(e,{method:"DELETE",...t})}};var b={};R(b,{convertToOdinAmount:()=>I,createTokenValidators:()=>c});function I(n,e={decimals:3,divisibility:8}){let t=e.decimals+e.divisibility;if(n=typeof n=="string"?n.trim():String(n),isNaN(+n))return 0n;let[i,r=""]=n.split(".");return r=r.slice(0,t),r=r.padEnd(t,"0"),i=i.replace(/^0+(?=\d)/,""),i===""&&(i="0"),BigInt(i+r)}var c={name:P,image:L,ticker:B,description:S,twitter:$,website:q,telegram:D};function P(n){if(typeof n!="string")return "Name must be a string.";if(!n||n.trim()==="")return "Name is required.";if(n.length<3||n.length>30)return "Name must be between 3 and 30 characters."}function S(n){if(n){if(typeof n!="string")return "Description must be a string.";if(n&&n.length>100)return "Description must not exceed 100 characters."}}function B(n){if(typeof n!="string")return "Ticker must be a string.";if(!n||n.trim()==="")return "Ticker is required.";if(n.length<3||n.length>10)return "Ticker must be between 3 and 10 characters.";if(!/^[A-Z0-9]+$/.test(n))return "Ticker must be alphanumeric characters in uppercase only.";if(!/(?=(.*[A-Z].*[A-Z]))/.test(n))return "Ticker must have at least 2 alpha characters."}function L(n){if(!(n instanceof File))return "Image must be a file.";let e=["image/png","image/jpeg","image/webp","image/svg","image/gif","image/avif"],t=200*1024;if(!n)return "Image file is required.";if(!e.includes(n.type))return `Invalid file type. Allowed types are: ${e.join(", ")}`;if(n.size>t)return `File size exceeds the maximum limit of ${t/1024}KB.`}function $(n){if(n){if(typeof n!="string")return "Twitter handle must be a string.";if(n&&!/^https?:\/\/(www\.)?(twitter\.com|x\.com)\/[A-Za-z0-9_]{1,15}\/?$/.test(n))return "Twitter must be a valid twitter URL."}}function q(n){if(n){if(typeof n!="string")return "Website must be a string.";if(n&&!/^(https?:\/\/)?([\w-]+(\.[\w-]+)+)(\/[\w-./?%&=]*)?$/.test(n))return "Website must be a valid URL."}}function D(n){if(n){if(typeof n!="string")return "Telegram must be a string.";if(n&&!/^(https?:\/\/)?(t\.me|telegram\.me|telegram\.org)\/[A-Za-z0-9_]{5,32}\/?$/.test(n))return "Telegram must be a valid telegram URL."}}var F={dev:"https://api.odin.fun/dev",prod:"https://api.odin.fun/v1",local:"https://api.odin.fun/dev"},m=class{_apiKey=null;_httpClient;BASE_URL;constructor(e="prod"){this._httpClient=new u,this.BASE_URL=F[e];}getUser(e){return this._httpClient.get(`${this.BASE_URL}/user/${e}`)}async getBalances(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/balances`,{params:{...t}})).data}async getBalance(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/balances`,{params:{token_in:t}})).data.find(r=>r.id===t)??null}getTokens(e,t={field:"marketcap",direction:"desc"},i={}){return this._httpClient.get(`${this.BASE_URL}/tokens`,{params:{...e,sort:`${t.field}:${t.direction}`,...i}})}getToken(e){return this._httpClient.get(`${this.BASE_URL}/token/${e}`)}async uploadImage(e){if(!this._apiKey)throw new Error("API key is not set");try{let t=c.image?.(e);if(t)throw new Error(t);let i=new FormData;return i.append("file",e),(await this._httpClient.post(`${this.BASE_URL}/upload`,i,{headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${this._apiKey}`}})).data.upload}catch(t){if(t instanceof Error)if(t.name==="AxiosError"){let i=t;throw new Error(i.response?.data?.message||i.message)}else throw new Error(t.message);else throw new Error("Image upload failed")}}updateTokenImage(e,t){if(!this._apiKey)throw new Error("API key is not set");try{return this._httpClient.post(`${this.BASE_URL}/token/${e}/image`,{image:t},{headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${this._apiKey}`}})}catch(i){if(i instanceof Error&&i.name==="AxiosError"){let r=i;throw new Error(r.response?.data?.message||r.message)}else throw new Error("Image update failed")}}getUserActivity(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/activity`,{params:{...t}})}async getUserTokens(e,t){let i=await this._httpClient.get(`${this.BASE_URL}/user/${e}/tokens`,{params:{...t}});return {...i,data:i.data.map(r=>({...r,balance:BigInt(r.balance)}))}}async getUserLiquidity(e,t){let i=await this._httpClient.get(`${this.BASE_URL}/user/${e}/liquidity`,{params:{...t}});return {...i,data:i.data.map(r=>({...r,balance:BigInt(r.balance)}))}}async getUserAchievements(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/achievements`,{params:{...t}})).data}getUserTransactions(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/transactions`,{params:{...t}})}getUserCreatedTokens(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/created`,{params:{...t}})}getUserStats(e){return this._httpClient.get(`${this.BASE_URL}/user/${e}/stats`)}set apiKey(e){this._apiKey=e;}get apiKey(){return this._apiKey}};var g=class{_identity;_api;_principal;_odin;constructor(e,t,i,r){this._principal=e,this._identity=t,this._api=i,this._odin=r;}get principal(){return this._principal}set principal(e){this._principal=e;}getIdentity(){return this._identity}getUser(){return this._api.getUser(this.principal)}getBalances(e){return this._api.getBalances(this.principal,e)}getBalance(e){return this._api.getBalance(this.principal,e)}getTokens(e){return this._api.getUserTokens(this.principal,e)}getCreatedTokens(e){return this._api.getUserCreatedTokens(this.principal,e)}getActivity(e){return this._api.getUserActivity(this.principal,e)}getLiquidity(e){return this._api.getUserLiquidity(this.principal,e)}getAchievements(e){return this._api.getUserAchievements(this.principal,e)}getTransactions(e){return this._api.getUserTransactions(this.principal,e)}getStats(){return this._api.getUserStats(this.principal)}sell(e){return this._odin.sell({...e,principal:this.principal})}buy(e){return this._odin.buy({...e,principal:this.principal})}addLiquidity(e){return this._odin.addLiquidity({...e,principal:this.principal})}removeLiquidity(e){return this._odin.removeLiquidity({...e,principal:this.principal})}transfer(e){return this._odin.transfer({...e,principal:this.principal})}createToken(e){return this._odin.createToken({...e,principal:this.principal})}icrcApprove(e){return this._odin.icrcApprove({...e,principal:this.principal})}swap(e){return this._odin.swap({...e,principal:this.principal})}};var w={local:"http://localhost:5173",prod:"https://odin.fun",dev:"https://dev.odin.fun",_preview:"https://deploy-preview-1368--dev-odin-toniq.netlify.app"};var h=class{_settings;constructor(e){this._settings={target:e?.target||"_blank",settings:e?.settings||""};}get settings(){return this._settings}set settings(e){this._settings={...this._settings,...e};}open(e){return window.open(e,this._settings?.target||"_blank",this._settings?.settings)}};var f=class{_window;_appInfo;_api;origin;constructor(e,t,i,r){this._window=e,this._api=t,this._appInfo=i,this.origin=r;}get appInfo(){return this._appInfo}createUrl(e){let t=new URL(`${this.origin}/${e}`);return this._appInfo?.name&&t.searchParams.append("app_name",this._appInfo.name),t.searchParams.append("referrer",window.location.origin),t}baseAction({params:e,odinPath:t,receivedMessageFromOrigin:i,resolve:r}){return new Promise((o,l)=>{let p=async s=>{s.origin===this.origin&&s.data.path==="/"+t&&(window.removeEventListener("message",p),(typeof i=="function"?i(s.data.message):i===s.data.message)?o(r.success(s.data.message)):l(new Error(r.failure)));},d=this.createUrl(t);for(let s in e)e[s]&&d.searchParams.append(s,e[s]);let a=this._window.open(d);if(!a||a.closed||typeof a.closed>"u"){l(new Error(r.didnotopen??`Failed to open ${t} window, please always allow popups and try again`));return}window.addEventListener("message",p);})}sell({token:e,tokenAmount:t,principal:i}){return this.baseAction({params:{principal:i,token:e,amount:t.toString()},odinPath:"authorize/sell",receivedMessageFromOrigin:"sold",resolve:{success:()=>true,failure:"Sell failed or was cancelled",close:"User closed the window"}})}buy({principal:e,token:t,btcAmount:i}){return this.baseAction({params:{principal:e,token:t,amount:i.toString()},odinPath:"authorize/buy",receivedMessageFromOrigin:"purchased",resolve:{success:()=>true,failure:"Purchase failed or was cancelled",close:"User closed the window"}})}transfer({principal:e,token:t,amount:i,destination:r}){return this.baseAction({params:{principal:e,token:t,amount:i.toString(),destination:r},odinPath:"authorize/transfer",receivedMessageFromOrigin:"transferred",resolve:{success:()=>true,failure:"Transfer failed or was cancelled",close:"User closed the window"}})}addLiquidity({principal:e,btcAmount:t,token:i}){return this.baseAction({params:{principal:e,amount:t.toString(),token:i},odinPath:"authorize/add_liquidity",receivedMessageFromOrigin:"addedLiquidity",resolve:{success:()=>true,failure:"Add liquidity failed or was cancelled",close:"User closed the window"}})}removeLiquidity({principal:e,lpAmount:t,token:i}){return this.baseAction({params:{principal:e,amount:t.toString(),token:i},odinPath:"authorize/remove_liquidity",receivedMessageFromOrigin:"removedLiquidity",resolve:{success:()=>true,failure:"Remove liquidity failed or was cancelled",close:"User closed the window"}})}swap({principal:e,fromToken:t,toToken:i,fromAmount:r}){return this.baseAction({params:{principal:e,from:t,to:i,amount:r.toString()},odinPath:"authorize/swap",receivedMessageFromOrigin:"swapped",resolve:{success:()=>true,failure:"Swap failed or was cancelled",close:"User closed the window"}})}icrcApprove({principal:e,token:t,spender:i,amount:r}){return this.baseAction({params:{principal:e,token:t,spender:i,amount:r.toString()},odinPath:"authorize/icrc_approve",receivedMessageFromOrigin:"approved",resolve:{success:()=>true,failure:"ICRC approve failed or was cancelled",close:"User closed the window"}})}async createToken({image:e,...t}){for(let o in c)if(o in t){let p=c[o]?.(t[o]||null);if(p)throw new Error(p)}if(t.discount&&!/^[A-Za-z0-9]{10}$/.test(t.discount))throw new Error("Discount code must be alphanumeric and exactly 10 characters long.");let i=await this._api.uploadImage(e);if(!await this.baseAction({params:{...t,image:i,buy:t.buy?.toString()},odinPath:"authorize/create_token",receivedMessageFromOrigin:"tokenCreated",resolve:{success:()=>true,failure:"Token creation failed or was cancelled",close:"User closed the window"}}))throw new Error("Token creation failed. Please try again.");return true}};var _=class{_appInfo=null;_api;_window;_odin;constructor(e){this._appInfo={env:"prod",name:"app_name",...e},this._api=new m(this._appInfo.env==="prod"?"prod":"dev"),this._window=new h,this._odin=new f(this._window,this._api,this._appInfo,w[this._appInfo.env||"prod"]);}createUrl(e){let t=new URL(`${this.origin}/${e}`);return this._appInfo?.name&&t.searchParams.append("app_name",this._appInfo.name),t.searchParams.append("referrer",window.location.origin),t}get origin(){return w[this._appInfo?.env||"prod"]}get appInfo(){return this._appInfo}connect({open:e,requires_api:t,requires_delegation:i,targets:r}={requires_delegation:false,requires_api:false}){return new Promise((o,l)=>{e&&(this._window.settings=e);let p=Ed25519KeyIdentity.generate(),d=async s=>{if(s.origin===this.origin&&s.data.path==="/authorize/connect")if(window.removeEventListener("message",d),s.data.message!="rejected"){let y;try{let T=s.data.message,{principal:v,jwt:O,delegationChain:A}=T;if(t&&(this._api.apiKey=O),i){if(!A)throw new Error("Delegation chain is missing");let k=DelegationIdentity.fromDelegation(p,DelegationChain.fromJSON(A));y=new g(v,k,this.api,this._odin);}else y=new g(v,null,this.api,this._odin);o(y);}catch{l(new Error("Failed to fetch user data"));}}else l(new Error("User rejected the connection"));},a=this.createUrl("authorize/connect");if(a.searchParams.append("requires_api",t?"1":"0"),i){a.searchParams.append("requires_delegation","1");let s=btoa(JSON.stringify(p.toJSON()));a.searchParams.append("session_key",s),a.searchParams.append("targets",r?.join(",")||"");}this._window.open(a),window.addEventListener("message",d);})}get api(){return this._api}get odin(){return this._odin}get currentEnv(){return this._appInfo?.env||"prod"}hello(){console.log("Hello from Odin Connect!");}};
2
- export{_ as OdinConnect,b as OdinUtils};
1
+ import P from'axios';import B from'@apimatic/json-bigint';import {DelegationChain,Ed25519KeyIdentity,DelegationIdentity}from'@dfinity/identity';var x=Object.defineProperty;var E=(i,e)=>{for(var t in e)x(i,t,{get:e[t],enumerable:true});};var L=i=>{try{return B.parse(i)}catch(e){return console.error("Error parsing response:",e),i}},m=class{async get(e,t){return this.fetch(e,{method:"GET",...t})}async fetch(e,t){let n={transformResponse:[L],...t};return (await P(e,n)).data}async post(e,t,n){return this.fetch(e,{method:"POST",data:t,...n})}async put(e,t,n){return this.fetch(e,{method:"PUT",data:t,...n})}async delete(e,t){return this.fetch(e,{method:"DELETE",...t})}};var k={};E(k,{convertToOdinAmount:()=>D,createTokenValidators:()=>c,isDelegationValid:()=>u});function u(i){try{let e=DelegationChain.fromJSON(JSON.parse(i)),t=BigInt(Date.now())*1000000n;return e.delegations.every(n=>n.delegation.expiration==null||n.delegation.expiration>t)}catch{return false}}function D(i,e={decimals:3,divisibility:8}){let t=e.decimals+e.divisibility;if(i=typeof i=="string"?i.trim():String(i),isNaN(+i))return 0n;let[n,r=""]=i.split(".");return r=r.slice(0,t),r=r.padEnd(t,"0"),n=n.replace(/^0+(?=\d)/,""),n===""&&(n="0"),BigInt(n+r)}var c={name:q,image:z,ticker:N,description:F,twitter:K,website:W,telegram:V};function q(i){if(typeof i!="string")return "Name must be a string.";if(!i||i.trim()==="")return "Name is required.";if(i.length<3||i.length>30)return "Name must be between 3 and 30 characters."}function F(i){if(i){if(typeof i!="string")return "Description must be a string.";if(i&&i.length>100)return "Description must not exceed 100 characters."}}function N(i){if(typeof i!="string")return "Ticker must be a string.";if(!i||i.trim()==="")return "Ticker is required.";if(i.length<3||i.length>10)return "Ticker must be between 3 and 10 characters.";if(!/^[A-Z0-9]+$/.test(i))return "Ticker must be alphanumeric characters in uppercase only.";if(!/(?=(.*[A-Z].*[A-Z]))/.test(i))return "Ticker must have at least 2 alpha characters."}function z(i){if(!(i instanceof File))return "Image must be a file.";let e=["image/png","image/jpeg","image/webp","image/svg","image/gif","image/avif"],t=200*1024;if(!i)return "Image file is required.";if(!e.includes(i.type))return `Invalid file type. Allowed types are: ${e.join(", ")}`;if(i.size>t)return `File size exceeds the maximum limit of ${t/1024}KB.`}function K(i){if(i){if(typeof i!="string")return "Twitter handle must be a string.";if(i&&!/^https?:\/\/(www\.)?(twitter\.com|x\.com)\/[A-Za-z0-9_]{1,15}\/?$/.test(i))return "Twitter must be a valid twitter URL."}}function W(i){if(i){if(typeof i!="string")return "Website must be a string.";if(i&&!/^(https?:\/\/)?([\w-]+(\.[\w-]+)+)(\/[\w-./?%&=]*)?$/.test(i))return "Website must be a valid URL."}}function V(i){if(i){if(typeof i!="string")return "Telegram must be a string.";if(i&&!/^(https?:\/\/)?(t\.me|telegram\.me|telegram\.org)\/[A-Za-z0-9_]{5,32}\/?$/.test(i))return "Telegram must be a valid telegram URL."}}var J={dev:"https://api.odin.fun/dev",prod:"https://api.odin.fun/v1",local:"https://api.odin.fun/dev"},f=class{_apiKey=null;_httpClient;BASE_URL;constructor(e="prod"){this._httpClient=new m,this.BASE_URL=J[e];}getUser(e){return this._httpClient.get(`${this.BASE_URL}/user/${e}`)}async getBalances(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/balances`,{params:{...t}})).data}async getBalance(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/balances`,{params:{token_in:t}})).data.find(r=>r.id===t)??null}getTokens(e,t={field:"marketcap",direction:"desc"},n={}){return this._httpClient.get(`${this.BASE_URL}/tokens`,{params:{...e,sort:`${t.field}:${t.direction}`,...n}})}getToken(e){return this._httpClient.get(`${this.BASE_URL}/token/${e}`)}async uploadImage(e){if(!this._apiKey)throw new Error("API key is not set");try{let t=c.image?.(e);if(t)throw new Error(t);let n=new FormData;return n.append("file",e),(await this._httpClient.post(`${this.BASE_URL}/upload`,n,{headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${this._apiKey}`}})).data.upload}catch(t){if(t instanceof Error)if(t.name==="AxiosError"){let n=t;throw new Error(n.response?.data?.message||n.message)}else throw new Error(t.message);else throw new Error("Image upload failed")}}updateTokenImage(e,t){if(!this._apiKey)throw new Error("API key is not set");try{return this._httpClient.post(`${this.BASE_URL}/token/${e}/image`,{image:t},{headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${this._apiKey}`}})}catch(n){if(n instanceof Error&&n.name==="AxiosError"){let r=n;throw new Error(r.response?.data?.message||r.message)}else throw new Error("Image update failed")}}getUserActivity(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/activity`,{params:{...t}})}async getUserTokens(e,t){let n=await this._httpClient.get(`${this.BASE_URL}/user/${e}/tokens`,{params:{...t}});return {...n,data:n.data.map(r=>({...r,balance:BigInt(r.balance)}))}}async getUserLiquidity(e,t){let n=await this._httpClient.get(`${this.BASE_URL}/user/${e}/liquidity`,{params:{...t}});return {...n,data:n.data.map(r=>({...r,balance:BigInt(r.balance)}))}}async getUserAchievements(e,t){return (await this._httpClient.get(`${this.BASE_URL}/user/${e}/achievements`,{params:{...t}})).data}getUserTransactions(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/transactions`,{params:{...t}})}getUserCreatedTokens(e,t){return this._httpClient.get(`${this.BASE_URL}/user/${e}/created`,{params:{...t}})}getUserStats(e){return this._httpClient.get(`${this.BASE_URL}/user/${e}/stats`)}set apiKey(e){this._apiKey=e;}get apiKey(){return this._apiKey}};var g=class{_identity;_api;_principal;_odin;constructor(e,t,n,r){this._principal=e,this._identity=t,this._api=n,this._odin=r;}get principal(){return this._principal}set principal(e){this._principal=e;}getIdentity(){return this._identity}getUser(){return this._api.getUser(this.principal)}getBalances(e){return this._api.getBalances(this.principal,e)}getBalance(e){return this._api.getBalance(this.principal,e)}getTokens(e){return this._api.getUserTokens(this.principal,e)}getCreatedTokens(e){return this._api.getUserCreatedTokens(this.principal,e)}getActivity(e){return this._api.getUserActivity(this.principal,e)}getLiquidity(e){return this._api.getUserLiquidity(this.principal,e)}getAchievements(e){return this._api.getUserAchievements(this.principal,e)}getTransactions(e){return this._api.getUserTransactions(this.principal,e)}getStats(){return this._api.getUserStats(this.principal)}sell(e){return this._odin.sell({...e,principal:this.principal})}buy(e){return this._odin.buy({...e,principal:this.principal})}addLiquidity(e){return this._odin.addLiquidity({...e,principal:this.principal})}removeLiquidity(e){return this._odin.removeLiquidity({...e,principal:this.principal})}transfer(e){return this._odin.transfer({...e,principal:this.principal})}createToken(e){return this._odin.createToken({...e,principal:this.principal})}icrcApprove(e){return this._odin.icrcApprove({...e,principal:this.principal})}swap(e){return this._odin.swap({...e,principal:this.principal})}};var b={local:"http://localhost:5173",prod:"https://odin.fun",dev:"https://dev.odin.fun",_preview:"https://deploy-preview-1368--dev-odin-toniq.netlify.app"};var y=class{_settings;constructor(e){this._settings={target:e?.target||"_blank",settings:e?.settings||""};}get settings(){return this._settings}set settings(e){this._settings={...this._settings,...e};}open(e){return window.open(e,this._settings?.target||"_blank",this._settings?.settings)}};var w=class{_window;_appInfo;_api;origin;constructor(e,t,n,r){this._window=e,this._api=t,this._appInfo=n,this.origin=r;}get appInfo(){return this._appInfo}createUrl(e){let t=new URL(`${this.origin}/${e}`);return this._appInfo?.name&&t.searchParams.append("app_name",this._appInfo.name),t.searchParams.append("referrer",window.location.origin),t}baseAction({params:e,odinPath:t,receivedMessageFromOrigin:n,resolve:r}){return new Promise((p,l)=>{let a=async s=>{s.origin===this.origin&&s.data.path==="/"+t&&(window.removeEventListener("message",a),(typeof n=="function"?n(s.data.message):n===s.data.message)?p(r.success(s.data.message)):l(new Error(r.failure)));},d=this.createUrl(t);for(let s in e)e[s]&&d.searchParams.append(s,e[s]);let o=this._window.open(d);if(!o||o.closed||typeof o.closed>"u"){l(new Error(r.didnotopen??`Failed to open ${t} window, please always allow popups and try again`));return}window.addEventListener("message",a);})}sell({token:e,tokenAmount:t,principal:n}){return this.baseAction({params:{principal:n,token:e,amount:t.toString()},odinPath:"authorize/sell",receivedMessageFromOrigin:"sold",resolve:{success:()=>true,failure:"Sell failed or was cancelled",close:"User closed the window"}})}buy({principal:e,token:t,btcAmount:n}){return this.baseAction({params:{principal:e,token:t,amount:n.toString()},odinPath:"authorize/buy",receivedMessageFromOrigin:"purchased",resolve:{success:()=>true,failure:"Purchase failed or was cancelled",close:"User closed the window"}})}transfer({principal:e,token:t,amount:n,destination:r}){return this.baseAction({params:{principal:e,token:t,amount:n.toString(),destination:r},odinPath:"authorize/transfer",receivedMessageFromOrigin:"transferred",resolve:{success:()=>true,failure:"Transfer failed or was cancelled",close:"User closed the window"}})}addLiquidity({principal:e,btcAmount:t,token:n}){return this.baseAction({params:{principal:e,amount:t.toString(),token:n},odinPath:"authorize/add_liquidity",receivedMessageFromOrigin:"addedLiquidity",resolve:{success:()=>true,failure:"Add liquidity failed or was cancelled",close:"User closed the window"}})}removeLiquidity({principal:e,lpAmount:t,token:n}){return this.baseAction({params:{principal:e,amount:t.toString(),token:n},odinPath:"authorize/remove_liquidity",receivedMessageFromOrigin:"removedLiquidity",resolve:{success:()=>true,failure:"Remove liquidity failed or was cancelled",close:"User closed the window"}})}swap({principal:e,fromToken:t,toToken:n,fromAmount:r}){return this.baseAction({params:{principal:e,from:t,to:n,amount:r.toString()},odinPath:"authorize/swap",receivedMessageFromOrigin:"swapped",resolve:{success:()=>true,failure:"Swap failed or was cancelled",close:"User closed the window"}})}icrcApprove({principal:e,token:t,spender:n,amount:r}){return this.baseAction({params:{principal:e,token:t,spender:n,amount:r.toString()},odinPath:"authorize/icrc_approve",receivedMessageFromOrigin:"approved",resolve:{success:()=>true,failure:"ICRC approve failed or was cancelled",close:"User closed the window"}})}async createToken({image:e,...t}){for(let p in c)if(p in t){let a=c[p]?.(t[p]||null);if(a)throw new Error(a)}if(t.discount&&!/^[A-Za-z0-9]{10}$/.test(t.discount))throw new Error("Discount code must be alphanumeric and exactly 10 characters long.");let n=await this._api.uploadImage(e);if(!await this.baseAction({params:{...t,image:n,buy:t.buy?.toString()},odinPath:"authorize/create_token",receivedMessageFromOrigin:"tokenCreated",resolve:{success:()=>true,failure:"Token creation failed or was cancelled",close:"User closed the window"}}))throw new Error("Token creation failed. Please try again.");return true}};var _=class{_key;constructor(e,t){this._key=`odin_connect:${e}:${t}:session`;}save(e){try{localStorage.setItem(this._key,JSON.stringify(e));}catch{}}load(){try{let e=localStorage.getItem(this._key);if(!e)return null;let t=JSON.parse(e);return this.isValidSessionData(t)?t:null}catch{return null}}isValidSessionData(e){if(typeof e!="object"||e===null)return false;let t=e;return typeof t.principal=="string"&&typeof t.sessionKey=="string"&&(t.delegationChain===null||typeof t.delegationChain=="string")&&(t.jwt===null||typeof t.jwt=="string")}clear(){try{localStorage.removeItem(this._key);}catch{}}};function j(i){let e=0;for(let t=0;t<i.length;t++)e=e*31+i.charCodeAt(t)|0;return (e>>>0).toString(36).slice(0,3).padStart(3,"0")}function M(i){return `${i.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}-${j(i)}`}var O=class{_appInfo=null;_api;_window;_odin;_storage;constructor(e){this._appInfo={env:"prod",name:"app_name",...e},this._appInfo.slug=this._appInfo.slug||M(this._appInfo.name),this._api=new f(this._appInfo.env==="prod"?"prod":"dev"),this._window=new y,this._odin=new w(this._window,this._api,this._appInfo,b[this._appInfo.env||"prod"]),this._storage=new _(this._appInfo.slug,this._appInfo.env||"prod");}createUrl(e){let t=new URL(`${this.origin}/${e}`);return this._appInfo?.name&&t.searchParams.append("app_name",this._appInfo.name),t.searchParams.append("referrer",window.location.origin),t}get origin(){return b[this._appInfo?.env||"prod"]}get appInfo(){return this._appInfo}get slug(){return this._appInfo?.slug||""}connect({open:e,requires_api:t,requires_delegation:n,targets:r}={requires_delegation:false,requires_api:false}){return new Promise((p,l)=>{e&&(this._window.settings=e);let a=Ed25519KeyIdentity.generate(),d=async s=>{if(s.origin===this.origin&&s.data.path==="/authorize/connect")if(window.removeEventListener("message",d),s.data.message!="rejected"){let v;try{let A=s.data.message,{principal:T,jwt:C,delegationChain:h}=A;if(t&&(this._api.apiKey=C),n){if(!h)throw new Error("Delegation chain is missing");let I=DelegationIdentity.fromDelegation(a,DelegationChain.fromJSON(h));v=new g(T,I,this.api,this._odin);}else v=new g(T,null,this.api,this._odin);(t||n)&&this._storage.save({principal:T,sessionKey:JSON.stringify(a.toJSON()),delegationChain:h?JSON.stringify(h):null,jwt:C||null}),p(v);}catch{l(new Error("Failed to fetch user data"));}}else l(new Error("User rejected the connection"));},o=this.createUrl("authorize/connect");if(o.searchParams.append("requires_api",t?"1":"0"),n){o.searchParams.append("requires_delegation","1");let s=btoa(JSON.stringify(a.toJSON()));o.searchParams.append("session_key",s),o.searchParams.append("targets",r?.join(",")||"");}this._window.open(o),window.addEventListener("message",d);})}get api(){return this._api}get odin(){return this._odin}get currentEnv(){return this._appInfo?.env||"prod"}restoreSession(){try{let e=this._storage.load();if(!e)return null;if(e.delegationChain&&!u(e.delegationChain))return this._storage.clear(),null;e.jwt&&(this._api.apiKey=e.jwt);let t=null;if(e.delegationChain&&e.sessionKey){let n=Ed25519KeyIdentity.fromJSON(e.sessionKey),r=DelegationChain.fromJSON(JSON.parse(e.delegationChain));t=DelegationIdentity.fromDelegation(n,r);}return new g(e.principal,t,this._api,this._odin)}catch{return this._storage.clear(),null}}disconnect(){this._storage.clear(),this._api.apiKey=null;}isSessionValid(){let e=this._storage.load();return e?e.delegationChain?u(e.delegationChain):true:false}hello(){console.log("Hello from Odin Connect!");}};
2
+ export{O as OdinConnect,k as OdinUtils};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odin-connect",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/readme.md CHANGED
@@ -12,6 +12,7 @@ A TypeScript SDK for integrating with the [Odin](https://odin.fun) decentralized
12
12
  - [API Request Flow](#api-request-flow)
13
13
  - [Getting Started](#getting-started)
14
14
  - [Authentication](#authentication)
15
+ - [Session Restoration](#session-restoration)
15
16
  - [Connected User Operations](#connected-user-operations)
16
17
  - [Fetching User Data](#fetching-user-data)
17
18
  - [Trading](#trading)
@@ -91,6 +92,7 @@ sequenceDiagram
91
92
  User->>Popup: Authenticates
92
93
  Popup->>SDK: postMessage({ principal, jwt, delegationChain? })
93
94
  SDK->>SDK: Create ConnectedUser instance
95
+ SDK->>SDK: Persist session to localStorage
94
96
  SDK->>App: Returns ConnectedUser
95
97
  ```
96
98
 
@@ -151,10 +153,11 @@ const odinConnect = new OdinConnect({
151
153
  env: "prod",
152
154
  });
153
155
 
154
- // 2. Authenticate a user
155
- const user = await odinConnect.connect({
156
- requires_api: true,
157
- });
156
+ // 2. Restore existing session or authenticate
157
+ let user = odinConnect.restoreSession();
158
+ if (!user) {
159
+ user = await odinConnect.connect({ requires_api: true });
160
+ }
158
161
 
159
162
  // 3. Fetch data
160
163
  const balances = await user.getBalances({ page: 1, limit: 10 });
@@ -204,6 +207,58 @@ const identity = user.getIdentity();
204
207
  // Use identity with @dfinity/agent
205
208
  ```
206
209
 
210
+ ## Session Restoration
211
+
212
+ OdinConnect automatically persists session data to `localStorage` after a successful `connect()`. This allows you to restore sessions on page load without requiring user action.
213
+
214
+ ### Restoring a session
215
+
216
+ ```typescript
217
+ const odinConnect = new OdinConnect({ name: "My App", env: "prod" });
218
+
219
+ // Attempt to restore a previous session (synchronous, no popup)
220
+ const user = odinConnect.restoreSession();
221
+ if (user) {
222
+ // Session restored — user is ready
223
+ const balances = await user.getBalances({ page: 1, limit: 10 });
224
+ } else {
225
+ // No valid session — prompt the user to connect
226
+ const user = await odinConnect.connect({ requires_api: true });
227
+ }
228
+ ```
229
+
230
+ ### Checking session validity
231
+
232
+ ```typescript
233
+ if (odinConnect.isSessionValid()) {
234
+ // A non-expired session exists in storage
235
+ }
236
+ ```
237
+
238
+ ### Disconnecting
239
+
240
+ ```typescript
241
+ // Clears persisted session data and resets the API key
242
+ odinConnect.disconnect();
243
+ ```
244
+
245
+ ### Custom app slug
246
+
247
+ Storage keys are scoped by a slug derived from your app name (e.g. `"My App"` becomes `"my-app"`). You can provide a custom slug to control the storage key:
248
+
249
+ ```typescript
250
+ const odinConnect = new OdinConnect({
251
+ name: "My App",
252
+ slug: "myapp-v2", // Storage key: odin_connect:myapp-v2:prod:session
253
+ env: "prod",
254
+ });
255
+ ```
256
+
257
+ > **Notes:**
258
+ > - Sessions with a delegation chain are automatically invalidated when the delegation expires.
259
+ > - Calling `disconnect()` in one tab clears the session for all tabs on the same origin.
260
+ > - In environments where `localStorage` is unavailable (SSR, strict privacy mode), session persistence is silently skipped.
261
+
207
262
  ## Connected User Operations
208
263
 
209
264
  After calling `connect()`, you receive a `ConnectedUser` with the following capabilities:
@@ -451,6 +506,7 @@ import type {
451
506
  OdinAchievement,
452
507
  OdinAchievementCategory,
453
508
  OdinConnectedUser,
509
+ SessionData,
454
510
  } from "odin-connect";
455
511
  ```
456
512