`
);
if (buttonText) {
btnContainer.appendChild(successAlert);
tipLinkMessageAndButtonContainer.appendChild(btnContainer);
}
tipLinkAlert.appendChild(tipLinkMessageAndButtonContainer);
innerContainer.appendChild(tipLinkAlert);
innerContainer.appendChild(closeButton);
modal.appendChild(innerContainer);
modal.appendChild(overlay);
const removePopupBlockedAlert = () => {
modal.remove();
if (
this.tipLinkAlertContainer &&
this.tipLinkAlertContainer.children.length === 0
) {
this.tipLinkAlertContainer.style.display = "none";
}
};
const bindOnLoad = () => {
btnContainer.addEventListener("click", () => {
onClick();
removePopupBlockedAlert();
});
};
const bindCloseButton = () => {
closeButton.addEventListener("click", () => {
removePopupBlockedAlert();
onClose();
});
overlay.addEventListener("mousedown", () => {
removePopupBlockedAlert();
onClose();
});
};
const attachOnLoad = () => {
if (this.tipLinkAlertContainer) {
this.tipLinkAlertContainer.appendChild(modal);
}
};
attachOnLoad();
bindCloseButton();
if (buttonText) {
bindOnLoad();
}
if (this.tipLinkAlertContainer) {
this.tipLinkAlertContainer.style.display = "block";
}
}
private getQueryParams(): Record {
const params: Record = {};
const queryString = window.location.search.slice(1);
queryString.split("&").forEach((pair) => {
const [key, value] = pair.split("=");
params[decodeURIComponent(key)] = decodeURIComponent(value);
});
return params;
}
private showIframe = () => {
if (this.tipLinkIframe) {
this.tipLinkIframe.style.display = "block";
}
};
async init({
directConnect,
autoConnect: autoConnect,
forceClickToContinue: forceClickToContinue,
showErrorMessage,
siwsInput,
theme,
hideDraggableWidget,
hideWalletOnboard,
onWalletHandshake,
}: {
directConnect: boolean;
autoConnect?: boolean;
forceClickToContinue?: boolean;
showErrorMessage?: boolean;
siwsInput?: CustomSolanaSignInInput;
theme?: TipLinkWalletAdapterTheme;
hideDraggableWidget?: boolean;
hideWalletOnboard?: boolean;
onWalletHandshake: (methods: {
showWallet: (page?: EmbeddedWalletPage) => void;
hideWallet: () => void;
}) => void;
}): Promise<{
pk: string;
siwsOutput?: SolanaSignInOutput;
}> {
if (this.isDisallowed()) {
this.notifyDisallowed();
return Promise.reject(new Error("Application not allowlisted"));
}
const queryParams = this.getQueryParams();
const promptTipLinkAutoConnectFromRedirect =
!!queryParams.promptTipLinkAutoConnect;
const tipLinkAutoConnect = !!queryParams.tipLinkAutoConnect;
if (promptTipLinkAutoConnectFromRedirect || tipLinkAutoConnect) {
directConnect = false;
autoConnect = true;
}
let windowParams: WindowOpenParams | undefined = undefined;
const isThemed = theme !== "system";
if (directConnect && !promptTipLinkAutoConnectFromRedirect) {
windowParams = this.windowCommunicator.openPopup(
`/embedded_adapter_login?ref=${window.location.origin}${
isThemed ? `&theme=${theme}` : ""
}`
);
if (!windowParams.popup || windowParams.popup.closed) {
directConnect = false;
forceClickToContinue = true;
}
}
const siwsInputPromise =
typeof siwsInput === "function"
? siwsInput()
: siwsInput
? Promise.resolve(siwsInput)
: undefined;
const tipLinkUrl = iFrameUrl({
buildEnv: this.buildEnv,
clientId: this.clientId,
walletAdapterNetwork: this._walletAdapterNetwork,
autoConnect,
tipLinkAutoConnect,
theme: theme && isThemed ? theme : undefined,
hideDraggableWidget,
hideWalletOnboard,
});
this.tipLinkIframe = htmlToElement(
``
);
const cssLink = new URL("/css/widget.css", getTipLinkUrl(this.buildEnv));
this.styleLink = htmlToElement(
``
);
this.tipLinkAlertContainer = htmlToElement(
``
);
this.tipLinkToastContainer = htmlToElement(
``
);
let fnsAtEnd: (() => void)[] = [];
let checkPopupClosed: NodeJS.Timeout | undefined = undefined;
let checkUrlForPausedExecution: NodeJS.Timeout | undefined = undefined;
let iframeNotLoading: NodeJS.Timeout | undefined = undefined;
let doCheckUrlForPausedExecution = false;
fnsAtEnd.push(() => {
clearInterval(checkPopupClosed);
clearInterval(checkUrlForPausedExecution);
clearInterval(iframeNotLoading);
});
const handleSetup = async (): Promise<{
pk: string;
siwsOutput?: SolanaSignInOutput;
}> => {
return new Promise<{
pk: string;
siwsOutput?: SolanaSignInOutput;
}>((resolve, reject) => {
// console.log("actually RUNNING handle setup");
if (directConnect) {
checkUrlForPausedExecution = setInterval(() => {
if (!doCheckUrlForPausedExecution) {
return;
}
try {
const url = this.tipLinkIframe?.contentWindow?.document.URL;
if (url === "about:blank") {
console.error("iframe is not loading");
// it is possible in mobile safari that the iframe in the background tab doesn't
// finish loading before the new window is opened. The window will then auto-close
// due to a timeout, and we'll show the login page instead.
clearInterval(checkUrlForPausedExecution);
iframeNotLoading = setInterval(() => {
windowParams?.popup.postMessage(
{ type: "iframe_not_loading" },
getTipLinkUrl(this.buildEnv)
);
}, 1_000);
}
} catch (error) {
// best effort, no need to handle
}
}, 300);
checkPopupClosed = setInterval(() => {
if (!windowParams?.popup || windowParams?.popup.closed) {
clearInterval(checkPopupClosed);
try {
const url = this.tipLinkIframe?.contentWindow?.document?.URL;
if (url === "about:blank") {
fnsAtEnd.forEach((fn) => fn());
fnsAtEnd = [];
this.clearElements();
this.init({
directConnect: false,
autoConnect: true,
forceClickToContinue: true,
showErrorMessage: true,
theme,
siwsInput,
hideDraggableWidget,
hideWalletOnboard,
onWalletHandshake,
})
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
});
return;
}
} catch {
// best effort, no need to handle
}
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "click_to_continue",
title: this.title,
}
);
this.showIframe();
}
}
}, 300);
}
try {
if (this.tipLinkIframe === undefined) {
throw Error("tipLinkIframe is undefined");
}
if (this.tipLinkAlertContainer === undefined) {
throw Error("tipLinkAlertContainer is undefined");
}
if (this.tipLinkToastContainer === undefined) {
throw Error("tipLinkToastContainer is undefined");
}
if (this.styleLink === undefined) {
throw Error("tipLinkStyles is undefined");
}
// const start = Date.now();
// console.log("appending child to iframe at:", start);
window.document.head.appendChild(this.styleLink);
window.document.body.appendChild(this.tipLinkIframe);
window.document.body.appendChild(this.tipLinkAlertContainer);
window.document.body.appendChild(this.tipLinkToastContainer);
if (this.isDisallowed()) {
this.notifyDisallowed();
windowParams?.popup?.close();
reject(new Error("Application not allowlisted"));
return;
}
doCheckUrlForPausedExecution = true;
let requestAnimationFrameTimeout: NodeJS.Timeout | undefined =
undefined;
const callback = (timestamp: number) => {
// console.log("timestamp", timestamp);
if (timestamp) {
clearTimeout(requestAnimationFrameTimeout);
doCheckUrlForPausedExecution = false;
setTimeout(() => {
// console.log(
// "started nested request animation frame",
// Date.now()
// );
// It is possible that the iframe loads a bit at first,
// so we set doCheckUrlForPausedExecution to false. However,
// we try requesting animation frame again shortly after
// and if times out, we set doCheckUrlForPausedExecution to true
// so that in the `checkUrlForPausedExecution` interval above it
// will indeed check. Note that we're not overly worried about
// this flag changing back to true unncessarily because we have the
// iframe url check against about:blank in the `checkUrlForPausedExecution`
// interval above
requestAnimationFrameTimeout = setTimeout(() => {
// console.log("Hit timeout at", Date.now());
doCheckUrlForPausedExecution = true;
}, 500);
window.requestAnimationFrame(callback);
}, 300);
}
};
window.requestAnimationFrame(callback);
this.tipLinkIframe.addEventListener("load", async () => {
if (!this.tipLinkIframe) {
throw Error("tipLinkIframe is undefined");
}
if (this.tipLinkIframe.contentWindow === null) {
throw Error("tipLinkIframe.contentWindow is null");
}
this.showIframe();
let postReadyInterval: NodeJS.Timeout | undefined = undefined;
let windowPost: PostFn | undefined = undefined;
if (windowParams?.popup && !windowParams.popup.closed) {
const { post, close } =
// TODO: this abstraction kind of sucks, this should not require an await
await this.windowCommunicator.setupHandshakeWithWindowParams(
windowParams,
{
window_ack: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
// console.log("received window ack", data);
clearInterval(postReadyInterval);
},
},
done: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
// console.log("!!received done", data);
this.showIframe();
close();
},
},
},
["done"]
);
// console.log("got window post function!!");
windowPost = post;
}
// console.log("!!! setting up handhsake with iframe!!");
const { close: closeIframeChannel } =
this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
ready: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
// console.log('received "ready" message from iframe', data);
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "ack",
title: this.title,
dAppSessionId: this.dAppSessionId,
tipLinkSessionId: this.tipLinkSessionId,
}
);
}
// TODO: use better flag to send to window post
// console.log("sending ready messages to window!");
windowPost?.({
type: "ready",
dAppSessionId: this.dAppSessionId,
tipLinkSessionId: this.tipLinkSessionId,
});
if (windowPost) {
postReadyInterval = setInterval(() => {
windowPost?.({
type: "ready",
dAppSessionId: this.dAppSessionId,
tipLinkSessionId: this.tipLinkSessionId,
});
}, 200);
}
if (
(directConnect &&
(!windowParams?.popup ||
windowParams.popup.closed)) ||
forceClickToContinue
) {
clearInterval(checkPopupClosed);
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "click_to_continue",
showErrorMessage,
title: this.title,
}
);
}
this.showIframe();
} else if (
!directConnect &&
!promptTipLinkAutoConnectFromRedirect
) {
clearInterval(checkPopupClosed);
// console.log("TRYING TO SINGLE POST!");
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "embedded_login",
}
);
}
// console.log(
// "displaying iframe for embedded_login after",
// Date.now() - start
// );
this.showIframe();
} else if (promptTipLinkAutoConnectFromRedirect) {
clearInterval(checkPopupClosed);
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "tiplink_autoconnect_from_redirect",
title: this.title,
}
);
}
// console.log(
// "displaying iframe for tiplink autoconnect from redirect after",
// Date.now() - start
// );
this.showIframe();
}
},
},
ready_for_tiplink_autoconnect: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
// console.log(
// "received ready_for_tiplink_autoconnect",
// data
// );
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "ack",
title: this.title,
}
);
}
},
},
loaded_public_key: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
clearInterval(checkPopupClosed);
// console.log("RECEIVED LOADED PUBLIC_KEY");
this.hideIframe();
if (this.tipLinkIframe?.contentWindow) {
this.windowCommunicator.singlePostToWindow(
this.tipLinkIframe.contentWindow,
{
type: "ack_loaded_public_key",
title: this.title,
dAppSessionId: this.dAppSessionId,
tipLinkSessionId: this.tipLinkSessionId,
}
);
}
// console.log(
// 'received "loaded_public_key" message from iframe',
// data.publicKey
// );
this.publicKeyString = data.publicKey;
if (siwsInputPromise) {
const siwsOutput = await this._signIn(
siwsInputPromise,
true
);
if (!siwsOutput) {
reject(new Error("missing siwsOutput"));
}
resolve({
pk: data.publicKey,
siwsOutput,
});
}
resolve({
pk: data.publicKey,
});
},
},
public_key: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
clearInterval(checkPopupClosed);
// console.log("RECEIVED PUBLIC_KEY");
this.hideIframe();
// console.log(
// 'received "public_key" message from iframe',
// data.publicKey
// );
this.publicKeyString = data.publicKey;
if (
promptTipLinkAutoConnectFromRedirect ||
tipLinkAutoConnect
) {
this.showTipLinkAutoconnectToast();
}
if (siwsInputPromise) {
const siwsOutput = await this._signIn(
siwsInputPromise,
true
);
if (!siwsOutput) {
reject(new Error("missing siwsOutput"));
}
resolve({
pk: data.publicKey,
siwsOutput,
});
}
resolve({
pk: data.publicKey,
});
},
},
cancel_connect: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
// console.log("RECEIVED CANCEL_CONNECT");
this.hideIframe();
windowParams?.popup?.close();
this.cleanUp();
reject(new Error("user clicked close button in iframe"));
},
},
focus_login: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
windowParams?.popup?.focus();
},
},
},
["public_key", "cancel_connect", "loaded_public_key"]
);
fnsAtEnd.push(closeIframeChannel);
});
} catch (error) {
// console.log("REJECTING error", error);
// console.log("rejected via errior");
reject(error);
}
});
};
// console.log("readying document");
await documentReady();
// console.log("handling setup");
return handleSetup()
.then((result) => {
fnsAtEnd.forEach((fn) => fn());
return result;
})
.then((result) => {
let handshake:
| {
post: PostFn;
close: CloseFn;
}
| undefined;
if (this.tipLinkIframe?.contentWindow) {
handshake = this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
show_wallet: {
type: CallbackType.DEFAULT,
cb: async () => {
this.showIframe();
},
},
hide_wallet: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideIframe();
},
},
hide_wallet_notification: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideWidgetNotificationUi();
},
},
show_wallet_notification: {
type: CallbackType.DEFAULT,
cb: async () => {
this.showWidgetNotificationUi();
},
},
},
[]
);
if (handshake) {
onWalletHandshake({
showWallet: (page?: EmbeddedWalletPage) => {
switch (page) {
case EmbeddedWalletPage.ADD_FUNDS:
case EmbeddedWalletPage.SWAP:
case EmbeddedWalletPage.WITHDRAW:
handshake?.post({ type: "show_wallet", page });
break;
case EmbeddedWalletPage.OVERVIEW:
default:
handshake?.post({ type: "show_wallet" });
}
},
hideWallet: () => {
handshake?.post({ type: "hide_wallet" });
},
});
}
this._walletHandshake = handshake;
}
// skip wallet widget setup if unwanted (effectively hides widget)
if (hideDraggableWidget) return result;
// setup wallet widget
this.tipLinkDraggableWidget = setupWalletWidget({
onDragStart: () => {
this._isDragging = true;
},
onDragEnd: () => {
this._isDragging = false;
},
isDragging: () => this._isDragging,
setInteractable: (interactable: Interact.Interactable) => {
this._interactable = interactable;
},
handleWidgetClick: () => {
if (this._walletHandshake) {
this._walletHandshake.post({ type: "show_wallet" });
} else {
// fallback re-establish handshake
if (this.tipLinkIframe?.contentWindow) {
const handshake:
| {
post: PostFn;
close: CloseFn;
}
| undefined =
this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
show_wallet: {
type: CallbackType.DEFAULT,
cb: async () => {
this.showIframe();
},
},
hide_wallet: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideIframe();
},
},
hide_wallet_notification: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideWidgetNotificationUi();
},
},
show_wallet_notification: {
type: CallbackType.DEFAULT,
cb: async () => {
this.showWidgetNotificationUi();
},
},
},
[]
);
if (handshake) {
onWalletHandshake({
showWallet: (page?: EmbeddedWalletPage) => {
switch (page) {
case EmbeddedWalletPage.ADD_FUNDS:
case EmbeddedWalletPage.SWAP:
case EmbeddedWalletPage.WITHDRAW:
handshake?.post({ type: "show_wallet", page });
break;
case EmbeddedWalletPage.OVERVIEW:
default:
handshake?.post({ type: "show_wallet" });
}
},
hideWallet: () => {
handshake?.post({ type: "hide_wallet" });
},
});
handshake.post({ type: "show_wallet" });
this._walletHandshake = handshake;
}
}
}
},
theme: this.theme,
windowDraggableResizeListener:
this.draggableWidgetWindowResizeListener,
windowDraggableScrollListener:
this.draggableWidgetWindowScrollListener,
tipLinkSessionId: this.tipLinkSessionId,
});
return result;
});
}
async cleanUp(): Promise {
// console.log("cleaning up");
if (this.tipLinkIframe) {
const iFrame = this.tipLinkIframe;
await new Promise((resolve) => {
const { post: postToIframe } =
this.windowCommunicator.setupHandshakeWithIframe(
iFrame,
{
disconnected: {
type: CallbackType.DEFAULT,
cb: async () => {
resolve();
},
},
},
["disconnected"]
);
postToIframe({ type: "disconnect" });
});
}
this.publicKeyString = undefined;
this.clearElements();
}
clearElements(): void {
localStorage.removeItem("tipLink_pk_connected");
tearDownWalletWidget(
this.draggableWidgetWindowResizeListener,
this.draggableWidgetWindowScrollListener,
this._interactable,
this.tipLinkDraggableWidget
);
if (
this.styleLink &&
isElement(this.styleLink) &&
window.document.head.contains(this.styleLink)
) {
this.styleLink.remove();
this.styleLink = undefined;
}
if (
this.tipLinkIframe &&
isElement(this.tipLinkIframe) &&
window.document.body.contains(this.tipLinkIframe)
) {
this.tipLinkIframe.remove();
this.tipLinkIframe = undefined;
}
if (
this.tipLinkAlertContainer &&
isElement(this.tipLinkAlertContainer) &&
window.document.body.contains(this.tipLinkAlertContainer)
) {
this.tipLinkAlertContainer.remove();
this.tipLinkAlertContainer = undefined;
}
if (
this.tipLinkToastContainer &&
isElement(this.tipLinkToastContainer) &&
window.document.body.contains(this.tipLinkToastContainer)
) {
this.tipLinkToastContainer.remove();
this.tipLinkToastContainer = undefined;
}
removePreviousWindowRef(TipLinkInstanceKey.EMBED);
}
// eslint-disable-next-line @typescript-eslint/require-await
async buildTransactionMessage(
transaction: Transaction | VersionedTransaction
): Promise<{
message: string;
}> {
if (isVersionedTransaction(transaction)) {
return {
message: Buffer.from(transaction.serialize()).toString("base64"),
};
}
return {
message: transaction
.serialize({ requireAllSignatures: false })
.toString("base64"),
};
}
async _signTransaction({
transaction,
doSend,
}: {
transaction: Transaction | VersionedTransaction;
doSend: boolean;
}): Promise {
this.extendSession();
// console.log("signing transaction");
const msg = await this.buildTransactionMessage(transaction);
return await new Promise((resolve, reject) => {
if (!this.tipLinkIframe) {
reject(new Error("iframe is missing"));
return;
}
this.showIframe();
const requestId = uuid();
const { post } = this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
signed_transaction: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
resolve(data.signed_transaction);
},
},
transaction_closed: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideIframe();
reject(new Error("User rejected transaction"));
},
},
sign_error: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
if (data && "message" in data) {
reject(new Error(data.message));
}
reject(new Error("Unknown error while signing transaction"));
},
},
},
["signed_transaction", "transaction_closed", "sign_error"],
requestId
);
post({
...msg,
type: "sign_transaction",
doSend,
requestId,
});
});
}
transactionFromString(
isVersioned: boolean,
signedTransactionMsg: string
): Transaction | VersionedTransaction {
if (isVersioned) {
return VersionedTransaction.deserialize(
Buffer.from(signedTransactionMsg, "base64")
);
} else {
return Transaction.from(Buffer.from(signedTransactionMsg, "base64"));
}
}
async signTransaction(
transaction: T
): Promise {
const signedTransaction = await this._signTransaction({
transaction,
doSend: false,
});
return this.transactionFromString(
isVersionedTransaction(transaction),
signedTransaction
) as T;
}
async signAllTransactions(
transactions: T[]
): Promise {
this.extendSession();
const messages = await Promise.all(
transactions.map(async (transaction) => {
const isVersioned = isVersionedTransaction(transaction);
const { message } = await this.buildTransactionMessage(transaction);
return {
message,
isVersioned,
};
})
);
const signedTxnMessages = await new Promise((resolve, reject) => {
if (!this.tipLinkIframe) {
reject(new Error("iframe is missing"));
return;
}
this.showIframe();
const requestId = uuid();
const { post } = this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
signed_transactions: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
resolve(data.signed_transactions);
},
},
transaction_closed: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideIframe();
reject(new Error("User rejected transaction"));
},
},
sign_error: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
if (data && "message" in data) {
reject(new Error(data.message));
} else {
reject(
new Error("Unknown error while signing transaction messages")
);
}
},
},
},
["signed_transactions", "transaction_closed", "sign_error"],
requestId
);
post({
type: "sign_all_transactions",
messages: messages.map((msg) => msg.message),
requestId,
});
});
return signedTxnMessages.map((signedTxnMsg, i) => {
return this.transactionFromString(
messages[i].isVersioned,
signedTxnMsg
) as T;
});
}
private async _signMessage(
message: Uint8Array,
type: string,
skipConfirm?: boolean
): Promise<{ data: Uint8Array; extraInfo: any }> {
return new Promise<{ data: Uint8Array; extraInfo: any }>(
(resolve, reject) => {
// TODO: do we still need to extend session
if (!this.tipLinkIframe) {
reject(new Error("iframe is missing"));
return;
}
this.extendSession();
const requestId = uuid();
const { post } = this.windowCommunicator.setupHandshakeWithIframe(
this.tipLinkIframe,
{
signed_message: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
const signedMessage = Buffer.from(
data.signed_message,
"base64"
);
resolve({ data: signedMessage, extraInfo: data.extraInfo });
},
},
message_closed: {
type: CallbackType.DEFAULT,
cb: async () => {
this.hideIframe();
reject(new Error("User rejected message"));
},
},
sign_error: {
type: CallbackType.DEFAULT,
cb: async (data: any) => {
this.hideIframe();
if (data && "message" in data) {
reject(new Error(data.message));
} else {
reject(new Error("Unknown error while signing message"));
}
},
},
},
["signed_message", "message_closed", "sign_error"],
requestId
);
this.showIframe();
post({
message: Buffer.from(message).toString("base64"),
type,
requestId,
skipConfirm,
});
}
);
}
async signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }> {
const { data } = await this._signMessage(message, "sign_message");
return { signature: data };
}
private async _signIn(
customInput?: Promise,
skipConfirm?: boolean
): Promise {
const input = await customInput;
const publicKeyAddress = input?.address || this.publicKeyString;
if (!publicKeyAddress) {
throw new Error("not connected!");
}
const domain = input?.domain || window.location.host;
if (!domain) {
throw new Error("no domain found!");
}
const siwsRequiredFields = {
...input,
domain,
address: publicKeyAddress,
} as SolanaSignInInputWithRequiredFields;
const signInMessage = createSignInMessage(siwsRequiredFields);
const { data: signature, extraInfo } = await this._signMessage(
signInMessage,
"siws",
skipConfirm
);
return {
account: new ReadonlyWalletAccount({
address: publicKeyAddress,
publicKey: new PublicKey(publicKeyAddress).toBytes(),
chains: [SOLANA_MAINNET_CHAIN],
// These must be included, otherwise the Standard Wallet Adapter
// will assume these features don't exist on the wallet adapter
features: [
SolanaSignAndSendTransaction,
SolanaSignTransaction,
SolanaSignMessage,
SolanaSignIn,
],
}),
signedMessage: signInMessage,
signature,
// @ts-ignore
extraInfo,
};
}
async signIn(
input?: Promise
): Promise {
return this._signIn(input);
}
// This is copied from the `sendTransaction` method in BaseSignerWalletAdapter,
// with the changes to use our own internal _signTransaction method, and to pass in the
// cluster nodes for the connectino that was passed into `sendTransaction`.
// we also don't emit an error since the method calling this is responsible for that.
async sendTransaction(
transaction: T,
prepareTransaction: (
transaction: Transaction,
connection: Connection,
sendOptions: Omit
) => Promise,
connection: Connection,
options: SendTransactionOptions = {}
): Promise {
if (isVersionedTransaction(transaction)) {
try {
const transactionString = await this._signTransaction({
transaction,
doSend: true,
});
const rawTransaction = Buffer.from(transactionString, "base64");
return await connection.sendRawTransaction(rawTransaction, options);
} catch (error: any) {
if (error instanceof WalletSignTransactionError) {
throw error;
}
throw new WalletSendTransactionError(error?.message, error);
}
} else {
try {
const { signers, ...sendOptions } = options;
const txn = await prepareTransaction(
transaction as Transaction,
connection,
sendOptions
);
signers?.length && txn.partialSign(...signers);
const transactionString = await this._signTransaction({
transaction: txn,
doSend: true,
});
const rawTransaction = Buffer.from(transactionString, "base64");
return await connection.sendRawTransaction(rawTransaction, sendOptions);
} catch (error: any) {
if (error instanceof WalletSignTransactionError) {
throw error;
}
throw new WalletSendTransactionError(error?.message, error);
}
}
}
notifyDisallowed() {
try {
this.clearElements();
} catch {
// best effort
}
console.error(
window.location.origin,
"not allowlisted – please contact the TipLink team at contact@tiplink.io to be added."
);
showDialog(
this.buildEnv,
`
${window.location.origin} does not have access yet ` +
"to use the TipLink Wallet Adapter. Please reach out to the " +
'TipLink team at ' +
"contact@tiplink.io or reach out via our " +
'' +
"discord.