rift-js
is the official JavaScript/TypeScript SDK and documentation hub for the Rift Protocol, a lightweight, embeddable Web3 action framework.
It enables iframe-based components (called Rift Frames) to securely communicate with Rift-compatible wallets (like Harpoon πͺ) to perform Flow blockchain interactions.
A Rift Frame is a secure, sandboxed iframe injected into any webpage via a rift://
URI.
-
Hosts a developer-controlled UI (like minting, voting, claiming, mini-games)
-
Rendered by wallets like Harpoon πͺ
-
Uses
rift-js
to:- Retrieve the user's Flow address
- Submit Flow transactions
- Execute read-only Cadence scripts
Share rift://
URIs anywhere (tweets, blogs, sites). If the user has Harpoon installed, it will recognize and render the interaction securely in-place.
Here's some bonus rift frames for development:
rift://localhost:3000?rift-color=4E71FF
rift://localhost:3000/nft-minter?rift-height=tall&rift-color=8200db
rift://localhost:3000/nft-carousel?rift-height=standard
rift://localhost:3000/token-buy?rift-height=compact&rift-color=00EF8B
rift-js/
βββ src/ # SDK source (TypeScript)
βββ starters/ # Starter projects
βββ docs/ # Protocol documentation
βββ rollup.config.js # Build config
βββ tsconfig.json
βββ package.json
βββ LICENSE
βββ README.md
npm install
npm run build
To use in a frame:
npm add rift-js
// Import from the root namespace
import { rift } from 'rift-js';
const instance = await rift();
console.log('Connected address:', await instance.getUserAddress());
// Submit a transaction
await instance.mutate({
cadence: `transaction { execute { log("hello") } }`,
args: [],
});
// Execute a script
const result = await instance.query({
cadence: `access(all) fun main(): String { return "Hello from script" }`,
args: [],
});
console.log('Script result:', result);
rift.on('tx:success', (txId) => console.log('Tx confirmed', txId));
rift.on('error', (err) => alert(err.message));
Wallet developers need to detect Rift URIs and handle communication with Rift Frames:
import { wallet } from 'rift-js';
// Create a detector to find Rift URIs in the page text content
const detector = new wallet.RiftDetector({
onRiftUriFound: (node, riftUrl, range) => {
// Handle the URI (e.g., ask user for permission)
handleRiftUri(node, riftUrl, range);
}
});
// Start scanning for Rift URIs
detector.start();
// Create an injector to replace URIs with iframes
const injector = new wallet.IframeInjector();
function handleRiftUri(node, riftUrl, range) {
// Get the parent element for replacement
const targetElement = range.commonAncestorContainer.parentElement;
// Inject the iframe
const iframe = injector.injectFrame(targetElement, riftUrl);
// Set up message handling for this iframe
if (iframe) {
setupMessageHandler(iframe);
}
}
function setupMessageHandler(iframe) {
window.addEventListener('message', (event) => {
// Check if message is from our iframe
if (event.source !== iframe.contentWindow) return;
const data = event.data;
if (!data || !data.type || !data.type.startsWith('rift:')) return;
// Handle handshake
if (data.type === 'rift:handshake') {
// Send context with user's address and network
const contextMessage = wallet.createContextMessage(
'0x1234567890abcdef', // User's address
wallet.NETWORKS.FLOW_TESTNET // Current network
);
iframe.contentWindow.postMessage(contextMessage, '*');
}
// Handle query (script execution)
else if (data.type === 'rift:intent' && data.action === 'query') {
// Execute the script using wallet infrastructure
executeScript(data.payload).then(result => {
const resultMessage = wallet.createScriptResultMessage(result);
iframe.contentWindow.postMessage(resultMessage, '*');
}).catch(error => {
const errorMessage = wallet.createErrorMessage(
wallet.ERROR_CODES.INVALID_PAYLOAD,
error.message
);
iframe.contentWindow.postMessage(errorMessage, '*');
});
}
// Handle mutate (transaction submission)
else if (data.type === 'rift:intent' && data.action === 'mutate') {
// Submit transaction using wallet infrastructure
submitTransaction(data.payload).then(txId => {
const resultMessage = wallet.createTransactionResultMessage(
'success',
txId
);
iframe.contentWindow.postMessage(resultMessage, '*');
}).catch(error => {
const errorMessage = wallet.createErrorMessage(
wallet.ERROR_CODES.USER_REJECTED,
error.message
);
iframe.contentWindow.postMessage(errorMessage, '*');
});
}
});
}
User opens a page containing a rift:// URI in text
β
Harpoon detects the URI and prompts for approval
β
User approves
β
Harpoon injects a secure iframe
β
Rift Frame loads and calls rift()
β
Handshake via postMessage
β
Harpoon responds with address and network context
β
Frame triggers tx or script intent
β
Harpoon signs, submits, or evaluates
β
Frame receives result or error event
Use the 'error'
event to catch runtime issues:
rift.on('error', (err) => {
console.error('Rift error:', err.message);
});
Code | Description |
---|---|
user_rejected |
User denied the action |
wallet_unavailable |
Wallet extension not detected |
timeout |
No response from wallet bridge |
invalid_payload |
Cadence or args were malformed |
connection_error |
Failed to connect to wallet |
not_initialized |
SDK not properly initialized |
unknown_error |
Unexpected error occurred |
not_supported |
Feature not supported |
A rift://
URI maps to an HTTPS iframe:
rift://domain.com/path?query=value
Injected as:
<iframe src="https://domain.com/path?query=value" sandbox="..." />
You can customize how your Rift Frame appears using special parameters with the rift-
prefix:
Control the height of your Rift Frame using the rift-height
parameter:
rift://app.example.com/mint?rift-height=tall&tokenId=123
Available height presets:
compact
(200px) - For simple confirmations or minimal UIstandard
(350px) - Default size for most interactionstall
(500px) - For complex interfaces like NFT minting
Control the color of your Rift Frame using the rift-color
parameter:
rift://app.example.com/mint?rift-color=4E71FF
The RiftBridge
class handles communication between the iframe and the wallet using window.postMessage()
. It implements:
- Handshake protocol
- Transaction submission
- Script execution
- Address retrieval
The EventEmitter
provides a simple event system for:
- Transaction lifecycle events (
tx:submitted
,tx:success
,tx:error
) - Error handling
- Context updates
The RiftDetector
helps with finding rift://
URIs in webpage text content.
The IframeInjector
handles creating and managing secure iframes for Rift content.
import { rift } from 'rift-js';
async function connectToWallet() {
const instance = await rift();
const address = await instance.getUserAddress();
console.log('Connected to address:', address);
}
import { rift } from 'rift-js';
async function sendTransaction() {
const instance = await rift();
try {
const txId = await instance.mutate({
cadence: `transaction { execute { log("Hello from Rift!") } }`,
args: [],
});
console.log('Transaction submitted:', txId);
} catch (error) {
console.error('Transaction failed:', error);
}
}
import { rift } from 'rift-js';
async function runScript() {
const instance = await rift();
const result = await instance.query({
cadence: `access(all) fun main(): String { return "Hello from script" }`,
args: [],
});
console.log('Script result:', result);
}
import { rift } from 'rift-js';
async function setupEventHandlers() {
const instance = await rift();
instance.on('ready', (data) => {
console.log('Connected to wallet:', data.address);
});
instance.on('tx:submitted', () => {
console.log('Transaction submitted to wallet');
});
instance.on('tx:success', (txId) => {
console.log('Transaction confirmed on chain:', txId);
});
instance.on('error', (error) => {
console.error('Error:', error.message);
});
}
import { RiftDetector } from 'rift-js';
const detector = new RiftDetector({
onRiftUriFound: (node, riftUrl, range) => {
console.log(`Found Rift URI: ${riftUrl}`);
// You can create clickable elements
const wrapper = document.createElement('span');
wrapper.className = 'rift-uri-highlight';
range.surroundContents(wrapper);
wrapper.addEventListener('click', () => {
console.log('URI clicked:', riftUrl);
});
}
});
detector.start();
import { RiftDetector, IframeInjector } from 'rift-js';
const injector = new IframeInjector({
defaultHeight: '400px',
onIframeInjected: (iframe, element) => {
console.log('Iframe injected:', iframe);
},
});
const detector = new RiftDetector({
onRiftUriFound: (node, riftUrl, range) => {
// Create a container at that position
const container = document.createElement('div');
range.surroundContents(container);
injector.injectFrame(container, riftUrl);
}
});
detector.start();
Communication between the iframe and wallet happens through postMessage
:
// Handshake to initialize connection
{ type: 'rift:handshake' }
// Transaction request
{
type: 'rift:intent',
action: 'submitTransaction',
payload: { cadence: '...', args: [] }
}
// Script execution
{
type: 'rift:intent',
action: 'executeScript',
payload: { cadence: '...', args: [] }
}
// Context with user address
{
type: 'rift:context',
address: '0x123',
network: 'flow-testnet'
}
// Transaction result
{
type: 'rift:mutateResult',
status: 'success',
txId: 'abc...'
}
// Script result
{
type: 'rift:queryResult',
result: "..."
}
// Error message
{
type: 'rift:error',
code: 'user_rejected',
message: 'User rejected transaction'
}
- Start the example frame:
cd starters/next-starter
npm install
npm run dev
npm run open:test-rift
- It will open the test-rift page linking to next-starter uris
- Harpoon detects the link, injects the iframe, and connects
By default, rift:// URLs are converted to https:// when injected as iframes. For local development or testing environments, you can enable HTTP instead:
import { setConfig } from 'rift-js';
// Enable HTTP for local development
setConfig({
useHttpForLocalDevelopment: true,
});
This will convert all rift://
URLs to http://
instead of https://
when useHttpForLocalDevelopment
is enabled.
We welcome contributions!
- Fork the repo
- Create a new branch
- Implement changes or additions
- Open a pull request with context
MIT