Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca701a48c2 | ||
|
|
94aae902a1 | ||
|
|
0c7149eb44 | ||
|
|
ce9220ea00 | ||
|
|
99c2af5e25 | ||
|
|
7495f46222 | ||
|
|
b8afa98de6 | ||
|
|
496615feae | ||
|
|
e2931df858 | ||
|
|
85bde21dc9 | ||
|
|
2c84ac4089 | ||
|
|
5ee449fc9c | ||
|
|
a94e9c2396 | ||
|
|
64f5e474bb | ||
|
|
aa0d23e966 | ||
|
|
b888413ca6 | ||
|
|
108a79ab3e | ||
|
|
51390f3af0 | ||
|
|
fb8c95a325 | ||
|
|
cf02cbf3af | ||
|
|
f152139d73 | ||
|
|
16307019be | ||
|
|
10fb338877 | ||
|
|
95412c75b9 | ||
|
|
7bc513c770 | ||
|
|
9069351e1a | ||
|
|
8f6363f4e4 | ||
|
|
a83104efff | ||
|
|
7936ebb6e9 | ||
|
|
3b986d52e5 | ||
|
|
ee4e13e098 | ||
|
|
8dc9e4102d | ||
|
|
33fc3e5226 | ||
|
|
ed47e93f1d | ||
|
|
1f7cda6698 |
@@ -47,6 +47,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"ogl": "^1.0.11",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
|
||||
@@ -691,3 +691,17 @@ export function reportProductFeedback(properties: {
|
||||
source: properties.source,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track conversions for the bridge page links
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*
|
||||
*/
|
||||
export function reportBridgePageLinkClick(params: {
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
}) {
|
||||
posthog.capture("bridge page link clicked", params);
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ const cached_getUniversalBridgeUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -636,7 +636,7 @@ const cached_getUniversalBridgeWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const vertex = `
|
||||
attribute vec2 position;
|
||||
void main(){gl_Position=vec4(position,0.0,1.0);}
|
||||
`;
|
||||
|
||||
const fragment = `
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uHueShift;
|
||||
uniform float uNoise;
|
||||
uniform float uScan;
|
||||
uniform float uScanFreq;
|
||||
uniform float uWarp;
|
||||
uniform vec3 uBackgroundColor;
|
||||
uniform float uBackgroundOpacity;
|
||||
uniform float uPatternLightness;
|
||||
#define iTime uTime
|
||||
#define iResolution uResolution
|
||||
|
||||
vec4 buf[8];
|
||||
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
|
||||
|
||||
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
|
||||
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
|
||||
|
||||
vec3 hueShiftRGB(vec3 col,float deg){
|
||||
vec3 yiq=rgb2yiq*col;
|
||||
float rad=radians(deg);
|
||||
float cosh=cos(rad),sinh=sin(rad);
|
||||
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
|
||||
return clamp(yiq2rgb*yiqShift,0.0,1.0);
|
||||
}
|
||||
|
||||
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
|
||||
|
||||
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
|
||||
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
|
||||
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
|
||||
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
|
||||
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
|
||||
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
|
||||
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
|
||||
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
|
||||
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
|
||||
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
|
||||
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
|
||||
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
|
||||
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
|
||||
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
|
||||
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
|
||||
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
|
||||
buf[0]=sigmoid(buf[0]);
|
||||
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor,in vec2 fragCoord){
|
||||
vec2 uv=fragCoord/uResolution.xy*2.-1.;
|
||||
uv.y*=-1.;
|
||||
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
|
||||
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec4 col;mainImage(col,gl_FragCoord.xy);
|
||||
col.rgb=hueShiftRGB(col.rgb,uHueShift);
|
||||
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
|
||||
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
|
||||
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
|
||||
|
||||
// Apply lightness control to pattern
|
||||
col.rgb = col.rgb * uPatternLightness;
|
||||
|
||||
// Use background color as base, then add pattern on top
|
||||
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
|
||||
|
||||
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
hueShift?: number;
|
||||
noiseIntensity?: number;
|
||||
scanlineIntensity?: number;
|
||||
speed?: number;
|
||||
scanlineFrequency?: number;
|
||||
warpAmount?: number;
|
||||
resolutionScale?: number;
|
||||
backgroundColor?: [number, number, number];
|
||||
backgroundOpacity?: number;
|
||||
patternLightness?: number;
|
||||
};
|
||||
|
||||
export function DarkVeil({
|
||||
hueShift = 0,
|
||||
noiseIntensity = 0,
|
||||
scanlineIntensity = 0,
|
||||
speed = 0.5,
|
||||
scanlineFrequency = 0,
|
||||
warpAmount = 0,
|
||||
resolutionScale = 1,
|
||||
backgroundColor = [0, 0, 0],
|
||||
backgroundOpacity = 1,
|
||||
patternLightness = 1.0,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
useEffect(() => {
|
||||
const canvas = ref.current as HTMLCanvasElement;
|
||||
const parent = canvas.parentElement as HTMLElement;
|
||||
|
||||
const renderer = new Renderer({
|
||||
dpr: Math.min(window.devicePixelRatio, 2),
|
||||
canvas,
|
||||
});
|
||||
|
||||
const gl = renderer.gl;
|
||||
const geometry = new Triangle(gl);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vec2() },
|
||||
uHueShift: { value: hueShift },
|
||||
uNoise: { value: noiseIntensity },
|
||||
uScan: { value: scanlineIntensity },
|
||||
uScanFreq: { value: scanlineFrequency },
|
||||
uWarp: { value: warpAmount },
|
||||
uBackgroundColor: { value: backgroundColor },
|
||||
uBackgroundOpacity: { value: backgroundOpacity },
|
||||
uPatternLightness: { value: patternLightness },
|
||||
},
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const resize = () => {
|
||||
const w = parent.clientWidth,
|
||||
h = parent.clientHeight;
|
||||
renderer.setSize(w * resolutionScale, h * resolutionScale);
|
||||
program.uniforms.uResolution.value.set(w, h);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const start = performance.now();
|
||||
let frame = 0;
|
||||
|
||||
const loop = () => {
|
||||
program.uniforms.uTime.value =
|
||||
((performance.now() - start) / 1000) * speed;
|
||||
program.uniforms.uHueShift.value = hueShift;
|
||||
program.uniforms.uNoise.value = noiseIntensity;
|
||||
program.uniforms.uScan.value = scanlineIntensity;
|
||||
program.uniforms.uScanFreq.value = scanlineFrequency;
|
||||
program.uniforms.uWarp.value = warpAmount;
|
||||
program.uniforms.uBackgroundColor.value = backgroundColor;
|
||||
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
|
||||
program.uniforms.uPatternLightness.value = patternLightness;
|
||||
renderer.render({ scene: mesh });
|
||||
frame = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, [
|
||||
hueShift,
|
||||
noiseIntensity,
|
||||
scanlineIntensity,
|
||||
speed,
|
||||
scanlineFrequency,
|
||||
warpAmount,
|
||||
resolutionScale,
|
||||
backgroundColor,
|
||||
backgroundOpacity,
|
||||
patternLightness,
|
||||
]);
|
||||
return <canvas ref={ref} className="w-full h-full block bg-background" />;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
}, [props.pageType]);
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl">
|
||||
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl min-w-0">
|
||||
<div className="flex gap-2.5 p-4 border-b border-dashed">
|
||||
<TabButton
|
||||
label="Swap"
|
||||
@@ -85,7 +85,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
<BuyWidget
|
||||
amount={props.buyAmount || "1"}
|
||||
chain={props.chain}
|
||||
className="!rounded-2xl !w-full !border-none"
|
||||
className="!rounded-2xl !border-none"
|
||||
title=""
|
||||
client={client}
|
||||
connectOptions={{
|
||||
@@ -179,7 +179,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
<SwapWidget
|
||||
client={client}
|
||||
theme={themeObj}
|
||||
className="!rounded-2xl !border-none !w-full"
|
||||
className="!rounded-2xl !border-none"
|
||||
prefill={{
|
||||
// buy this token by default
|
||||
buyToken: {
|
||||
|
||||
@@ -4,9 +4,10 @@ import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function ToggleThemeButton() {
|
||||
export function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
@@ -15,7 +16,10 @@ export function ToggleThemeButton() {
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full border border-border bg-background p-2",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FaqSection(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqItem(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpenn] = useState(false);
|
||||
const contentId = useId();
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className={cn("border-b border-dashed", props.className)}>
|
||||
<h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setIsOpenn(!isOpen)}
|
||||
aria-controls={contentId}
|
||||
aria-expanded={isOpen}
|
||||
className={cn(
|
||||
"w-full justify-between h-auto py-5 text-base text-muted-foreground hover:bg-transparent pl-0 text-wrap text-left gap-6",
|
||||
isOpen && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{props.title}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-4 shrink-0 transition-transform duration-200",
|
||||
isOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
"text-muted-foreground text-base leading-7 pb-6 max-w-4xl",
|
||||
!isOpen && "hidden",
|
||||
)}
|
||||
id={contentId}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export function GridPatternEmbedContainer(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-xl overflow-hidden relative">
|
||||
<div className="flex justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 sm:rounded-xl sm:overflow-hidden relative">
|
||||
<GridPattern
|
||||
width={30}
|
||||
height={30}
|
||||
@@ -17,7 +17,7 @@ export function GridPatternEmbedContainer(props: {
|
||||
"linear-gradient(to bottom right,white,transparent,transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="sm:w-[420px] z-10">{props.children}</div>
|
||||
<div className="z-10 min-w-0">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
type ChainSeo = {
|
||||
title: string;
|
||||
description: string;
|
||||
og: {
|
||||
title: string;
|
||||
description: string;
|
||||
site_name: string;
|
||||
url: string;
|
||||
};
|
||||
faqs: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
chain: {
|
||||
chainId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
nativeCurrency: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
testnet: boolean;
|
||||
is_deprecated: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchChainSeo = unstable_cache(
|
||||
async (chainId: number) => {
|
||||
const url = new URL(
|
||||
`https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`,
|
||||
);
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return res.json() as Promise<ChainSeo>;
|
||||
},
|
||||
["chain-seo"],
|
||||
{ revalidate: 60 * 60 * 24 }, // 24 hours
|
||||
);
|
||||
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Chain Overview" />
|
||||
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
|
||||
<div className="grid grid-cols-1 gap-6 rounded-xl border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-6">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CircleCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadataWithServices } from "@/types/chain";
|
||||
import { products } from "../../../../components/server/products";
|
||||
@@ -24,15 +23,18 @@ export function SupportedProductsSection(props: {
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
|
||||
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
|
||||
key={product.id}
|
||||
>
|
||||
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
|
||||
<product.icon className="mt-0.5 size-5 shrink-0" />
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<product.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={product.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ExplorersSection(props: {
|
||||
key={explorer.url}
|
||||
>
|
||||
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 font-semibold text-base capitalize">
|
||||
<h3 className="mb-1 font-medium text-base capitalize">
|
||||
{explorer.name}
|
||||
</h3>
|
||||
<Link
|
||||
|
||||
@@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-base text-muted-foreground">
|
||||
{props.title}
|
||||
</h3>
|
||||
<h3 className="text-base text-muted-foreground">{props.title}</h3>
|
||||
{props.titleIcon}
|
||||
</div>
|
||||
{props.children}
|
||||
|
||||
@@ -22,39 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
|
||||
import { StarButton } from "../../components/client/star-button";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
import { ChainPageView } from "./components/client/chain-pageview";
|
||||
import { ChainHeader } from "./components/server/chain-header";
|
||||
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
type Params = Promise<{ chain_id: string }>;
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ chain_id: string }>;
|
||||
}): Promise<Metadata> {
|
||||
params: Params;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const params = await props.params;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
const title = `${sanitizedChainName}: RPC and Chain Settings`;
|
||||
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
|
||||
chain.testnet && chain.faucets?.length ? "faucet options" : "more"
|
||||
}.`;
|
||||
if (!chainSeo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
description,
|
||||
title: chainSeo.title,
|
||||
description: chainSeo.description,
|
||||
metadataBase: new URL("https://thirdweb.com"),
|
||||
openGraph: {
|
||||
description,
|
||||
title,
|
||||
title: chainSeo.og.title,
|
||||
description: chainSeo.og.description,
|
||||
siteName: "thirdweb",
|
||||
type: "website",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
twitter: {
|
||||
title: chainSeo.og.title,
|
||||
description: chainSeo.og.description,
|
||||
card: "summary_large_image",
|
||||
creator: "@thirdweb",
|
||||
site: "@thirdweb",
|
||||
},
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
// this is the dashboard layout file
|
||||
export default async function ChainPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ chain_id: string }>;
|
||||
params: Params;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
@@ -67,8 +79,10 @@ export default async function ChainPageLayout(props: {
|
||||
redirect(chain.slug);
|
||||
}
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(chain.chainId);
|
||||
const chainSeo = await fetchChainSeo(chain.chainId);
|
||||
const client = getClientThirdwebClient(undefined);
|
||||
const description = customChainMetadata?.about || chainSeo?.description;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
@@ -123,7 +137,7 @@ export default async function ChainPageLayout(props: {
|
||||
<ChainHeader
|
||||
chain={chain}
|
||||
client={client}
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
headerImageUrl={customChainMetadata?.headerImgUrl}
|
||||
logoUrl={chain.icon?.url}
|
||||
/>
|
||||
|
||||
@@ -131,7 +145,7 @@ export default async function ChainPageLayout(props: {
|
||||
|
||||
<div className="flex flex-col gap-3 md:gap-2">
|
||||
{/* Gas Sponsored badge - Mobile */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
{customChainMetadata?.gasSponsored && (
|
||||
<div className="flex md:hidden">
|
||||
<GasSponsoredBadge />
|
||||
</div>
|
||||
@@ -153,7 +167,7 @@ export default async function ChainPageLayout(props: {
|
||||
)}
|
||||
|
||||
{/* Gas Sponsored badge - Desktop */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
{customChainMetadata?.gasSponsored && (
|
||||
<div className="hidden md:block">
|
||||
<GasSponsoredBadge />
|
||||
</div>
|
||||
@@ -161,9 +175,9 @@ export default async function ChainPageLayout(props: {
|
||||
</div>
|
||||
|
||||
{/* description */}
|
||||
{chainMetadata?.about && (
|
||||
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base">
|
||||
{chainMetadata.about}
|
||||
{description && (
|
||||
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base text-pretty max-w-3xl">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
import { BuyFundsSection } from "./components/client/BuyFundsSection";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
@@ -9,22 +11,27 @@ import { ExplorersSection } from "./components/server/explorer-section";
|
||||
import { FaucetSection } from "./components/server/FaucetSection";
|
||||
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
|
||||
|
||||
export default async function Page(props: {
|
||||
type Props = {
|
||||
params: Promise<{ chain_id: string }>;
|
||||
}) {
|
||||
};
|
||||
|
||||
export default async function Page(props: Props) {
|
||||
const params = await props.params;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(Number(params.chain_id));
|
||||
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
const client = getClientThirdwebClient();
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Custom CTA */}
|
||||
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
|
||||
<ChainCTA {...chainMetadata.cta} />
|
||||
{(customChainMetadata?.cta?.title ||
|
||||
customChainMetadata?.cta?.description) && (
|
||||
<ChainCTA {...customChainMetadata.cta} />
|
||||
)}
|
||||
|
||||
{/* Deprecated Alert */}
|
||||
@@ -58,6 +65,12 @@ export default async function Page(props: {
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
|
||||
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
|
||||
<div className="py-10">
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
|
||||
includeOwners: true,
|
||||
start: currentPage * pageSize,
|
||||
tokenByIndex,
|
||||
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { getChainMetadata } from "../../../utils";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListCardProps = {
|
||||
favoriteButton: JSX.Element | undefined;
|
||||
@@ -18,7 +18,7 @@ type ChainListCardProps = {
|
||||
iconUrl?: string;
|
||||
};
|
||||
|
||||
export async function ChainListCard({
|
||||
export function ChainListCard({
|
||||
isDeprecated,
|
||||
chainId,
|
||||
chainName,
|
||||
@@ -28,7 +28,7 @@ export async function ChainListCard({
|
||||
favoriteButton,
|
||||
iconUrl,
|
||||
}: ChainListCardProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(chainId);
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
@@ -90,9 +90,9 @@ export async function ChainListCard({
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{(isDeprecated || chainMetadata?.gasSponsored) && (
|
||||
{(isDeprecated || customChainMetadata?.gasSponsored) && (
|
||||
<div className="mt-5 flex gap-5 border-t pt-4">
|
||||
{!isDeprecated && chainMetadata?.gasSponsored && (
|
||||
{!isDeprecated && customChainMetadata?.gasSponsored && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TicketCheckIcon className="size-5 text-foreground" />
|
||||
<p className="text-sm">Gas Sponsored</p>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { getChainMetadata } from "../../../utils";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListRowProps = {
|
||||
favoriteButton: JSX.Element | undefined;
|
||||
@@ -26,7 +26,7 @@ type ChainListRowProps = {
|
||||
iconUrl?: string;
|
||||
};
|
||||
|
||||
export async function ChainListRow({
|
||||
export function ChainListRow({
|
||||
isDeprecated,
|
||||
chainId,
|
||||
chainName,
|
||||
@@ -36,7 +36,7 @@ export async function ChainListRow({
|
||||
favoriteButton,
|
||||
iconUrl,
|
||||
}: ChainListRowProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const chainMetadata = getCustomChainMetadata(chainId);
|
||||
return (
|
||||
<TableRow className="hover:bg-accent/50" linkBox>
|
||||
{/* Name */}
|
||||
|
||||
@@ -914,9 +914,9 @@ const chainMetaRecord = {
|
||||
} satisfies Record<number, ExtraChainMetadata>;
|
||||
// END TEMPORARY
|
||||
|
||||
export async function getChainMetadata(
|
||||
export function getCustomChainMetadata(
|
||||
chainId: number,
|
||||
): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> {
|
||||
): (ExtraChainMetadata & { gasSponsored?: true }) | null {
|
||||
// TODO: fetch this from the API
|
||||
if (chainId in chainMetaRecord) {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { ActivityIcon, SearchIcon, TrendingUpIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,11 +15,22 @@ const client = getClientThirdwebClient();
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
type Filter =
|
||||
| { type: "filter"; value: "volume" | "market_cap" }
|
||||
| { type: "search"; value: string };
|
||||
|
||||
export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [chainId, setChainId] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [sortBy, setSortBy] = useState<"volume" | "market_cap">("volume");
|
||||
const [filter, _setFilter] = useState<Filter>({
|
||||
type: "filter",
|
||||
value: "volume",
|
||||
});
|
||||
|
||||
const setFilter = useCallback((filter: Filter) => {
|
||||
_setFilter(filter);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const tokensQuery = useQuery({
|
||||
queryKey: [
|
||||
@@ -27,8 +38,7 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
{
|
||||
page,
|
||||
chainId,
|
||||
sortBy,
|
||||
search,
|
||||
filter,
|
||||
},
|
||||
],
|
||||
queryFn: () => {
|
||||
@@ -37,8 +47,8 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
chainId: chainId,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
sortBy: search ? undefined : sortBy,
|
||||
query: search ? search : undefined,
|
||||
sortBy: filter.type === "filter" ? filter.value : undefined,
|
||||
query: filter.type === "search" ? filter.value : undefined,
|
||||
});
|
||||
},
|
||||
refetchOnMount: false,
|
||||
@@ -62,19 +72,19 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
<SortButton
|
||||
label="Popular"
|
||||
onClick={() => {
|
||||
setSortBy("market_cap");
|
||||
setSearch("");
|
||||
setFilter({ type: "filter", value: "market_cap" });
|
||||
}}
|
||||
isSelected={sortBy === "market_cap" && !search}
|
||||
isSelected={
|
||||
filter.type === "filter" && filter.value === "market_cap"
|
||||
}
|
||||
icon={ActivityIcon}
|
||||
/>
|
||||
<SortButton
|
||||
label="Trending"
|
||||
onClick={() => {
|
||||
setSortBy("volume");
|
||||
setSearch("");
|
||||
setFilter({ type: "filter", value: "volume" });
|
||||
}}
|
||||
isSelected={sortBy === "volume" && !search}
|
||||
isSelected={filter.type === "filter" && filter.value === "volume"}
|
||||
icon={TrendingUpIcon}
|
||||
/>
|
||||
</div>
|
||||
@@ -83,9 +93,11 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search Tokens"
|
||||
value={search}
|
||||
value={filter.type === "search" ? filter.value : ""}
|
||||
className="rounded-xl bg-card flex-1 pl-9"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) =>
|
||||
setFilter({ type: "search", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +215,7 @@ export function ServerWalletsTableUI({
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage > 1 ? currentPage - 1 : 1
|
||||
}`}
|
||||
legacyBehavior
|
||||
@@ -232,7 +232,7 @@ export function ServerWalletsTableUI({
|
||||
(pageNumber) => (
|
||||
<PaginationItem key={`page-${pageNumber}`}>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationLink isActive={currentPage === pageNumber}>
|
||||
@@ -244,7 +244,7 @@ export function ServerWalletsTableUI({
|
||||
)}
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||
}`}
|
||||
passHref
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { DarkVeil } from "@/components/DarkVeil";
|
||||
|
||||
const bgBackground: Record<string, [number, number, number]> = {
|
||||
dark: [0, 0, 0],
|
||||
light: [250 / 255, 250 / 255, 250 / 255],
|
||||
};
|
||||
|
||||
const hues = {
|
||||
light: 65,
|
||||
dark: -85,
|
||||
};
|
||||
|
||||
const patternLightnesss = {
|
||||
light: -1,
|
||||
dark: 0.8,
|
||||
};
|
||||
|
||||
export function BridgeVeil() {
|
||||
const theme = useTheme();
|
||||
const isLight = theme.theme === "light";
|
||||
const patternLightness = isLight
|
||||
? patternLightnesss.light
|
||||
: patternLightnesss.dark;
|
||||
const hueShift = isLight ? hues.light : hues.dark;
|
||||
return (
|
||||
<DarkVeil
|
||||
backgroundColor={bgBackground[isLight ? "light" : "dark"]}
|
||||
patternLightness={patternLightness}
|
||||
hueShift={hueShift}
|
||||
warpAmount={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -16,13 +16,11 @@ export function UniversalBridgeEmbed({
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-[400px]">
|
||||
<BuyAndSwapEmbed
|
||||
chain={chain}
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
pageType="bridge"
|
||||
/>
|
||||
</div>
|
||||
<BuyAndSwapEmbed
|
||||
chain={chain}
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
pageType="bridge"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
import { ChevronRightIcon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportBridgePageLinkClick } from "@/analytics/report";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PillLink(props: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
}) {
|
||||
const Icon =
|
||||
props.linkType === "integrate-bridge" ? DollarSignIcon : TrendingUpIcon;
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
onClick={() => reportBridgePageLinkClick({ linkType: props.linkType })}
|
||||
className={cn(
|
||||
"shadow-sm text-center justify-center inline-flex items-center gap-4 text-foreground group",
|
||||
"text-sm bg-card/50 backdrop-blur-lg border border-border/70",
|
||||
"rounded-full px-4 py-2.5 transition-colors duration-300 text-pretty leading-5",
|
||||
"hover:bg-pink-300/20 dark:hover:bg-pink-950/30 hover:text-foreground hover:border-pink-400 dark:hover:border-pink-800",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icon className="size-4 shrink-0 text-pink-600" />
|
||||
{props.children}
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground/50 group-hover:translate-x-1 transition-all duration-300 group-hover:text-foreground" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
|
||||
|
||||
export function PageHeader(props: { containerClassName?: string }) {
|
||||
return (
|
||||
<div className="border-b border-dashed">
|
||||
<div className="border-b border-border/50 border-dashed">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-3",
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
@@ -30,7 +30,7 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
<ToggleThemeButton className="bg-transparent" />
|
||||
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
After Width: | Height: | Size: 104 KiB |
@@ -4,14 +4,17 @@ import type { Address } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { BridgeVeil } from "./components/BridgeVeil";
|
||||
import { PillLink } from "./components/client/pill-link";
|
||||
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
|
||||
import { PageHeader } from "./components/header";
|
||||
import { bridgeAppThirdwebClient } from "./constants";
|
||||
|
||||
const title = "thirdweb Payments: Swap, Bridge, and Onramp";
|
||||
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
|
||||
const description =
|
||||
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Payments.";
|
||||
"Bridge, swap, and on-ramp across 9M+ token routes on 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing and near-instant fast finality";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description,
|
||||
@@ -52,37 +55,97 @@ export default async function BridgePage({
|
||||
|
||||
return (
|
||||
<div className="grow flex flex-col relative overflow-hidden">
|
||||
<PageHeader />
|
||||
|
||||
<div className="flex grow flex-col items-center justify-center py-36 px-4 min-h-dvh relative overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed
|
||||
amount={amount as string}
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
token={
|
||||
symbol && decimals && tokenName
|
||||
? {
|
||||
address: tokenAddress as Address,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="absolute top-0 left-0 right-0 h-dvh md:h-[1200px] fade-in-0 animate-in duration-700">
|
||||
<BridgeVeil />
|
||||
</div>
|
||||
|
||||
<div className="bg-background relative z-10">
|
||||
<AppFooter />
|
||||
<div className="relative z-10">
|
||||
<PageHeader />
|
||||
|
||||
<div className="h-20" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="container">
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-none text-pretty">
|
||||
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
|
||||
chain, instantly
|
||||
</h1>
|
||||
|
||||
<div className="flex-wrap flex gap-3 items-center justify-center">
|
||||
<DataPill>85+ Chains Supported</DataPill>
|
||||
<DataPill>3200+ Tokens Supported</DataPill>
|
||||
<DataPill>9+ Million Routes Available</DataPill>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-16" />
|
||||
|
||||
<div className="flex grow items-center justify-center px-4 relative">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed
|
||||
amount={amount as string}
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
token={
|
||||
symbol && decimals && tokenName
|
||||
? {
|
||||
address: tokenAddress as Address,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="flex flex-col gap-4 items-center container">
|
||||
<PillLink
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
linkType="integrate-bridge"
|
||||
>
|
||||
Integrate Bridge in your apps in minutes, and start generating
|
||||
revenue
|
||||
</PillLink>
|
||||
|
||||
<PillLink
|
||||
href="https://thirdweb.com/tokens"
|
||||
linkType="trending-tokens"
|
||||
>
|
||||
Discover Trending Tokens
|
||||
</PillLink>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="container max-w-2xl">
|
||||
<BridgeFaqSection />
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="relative">
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataPill(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="flex items-center gap-1.5 text-foreground/50 text-xs bg-accent/30 backdrop-blur-lg border border-border/70 rounded-full px-3 py-1.5 hover:text-foreground transition-colors duration-300">
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 text-foreground/30 dark:text-foreground/10",
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-pink-700/30 dark:text-pink-500/20",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
@@ -94,3 +157,35 @@ function DotsBackgroundPattern(props: { className?: string }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bridgeFaqs: Array<{ title: string; description: string }> = [
|
||||
{
|
||||
title: "What is bridging in crypto?",
|
||||
description:
|
||||
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
|
||||
},
|
||||
{
|
||||
title: "How does crypto bridging work?",
|
||||
description:
|
||||
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
|
||||
},
|
||||
{
|
||||
title: "What is a crypto asset swap?",
|
||||
description:
|
||||
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
|
||||
},
|
||||
{
|
||||
title: "How can I get stablecoins like USDC or USDT?",
|
||||
description:
|
||||
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
|
||||
},
|
||||
{
|
||||
title: "What is the cost of bridging and swapping?",
|
||||
description:
|
||||
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
|
||||
},
|
||||
];
|
||||
|
||||
function BridgeFaqSection() {
|
||||
return <FaqSection faqs={bridgeFaqs} />;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
|
||||
@@ -5,6 +5,6 @@ export const maxDuration = 300;
|
||||
export async function GET(_req: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Congratulations! You have accessed the protected route.",
|
||||
message: "Payment successful. You have accessed the protected route.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AppSidebarLayout } from "./AppSidebar";
|
||||
import { Providers } from "./providers";
|
||||
import "@workspace/ui/global.css";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
const monoFont = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "thirdweb playground",
|
||||
metadataBase,
|
||||
@@ -37,8 +26,8 @@ export default async function RootLayout({
|
||||
<body
|
||||
className={cn(
|
||||
"bg-background font-sans antialiased ",
|
||||
sansFont.variable,
|
||||
monoFont.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TokenSelector } from "@/components/ui/TokenSelector";
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import type { TokenMetadata } from "@/lib/types";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
|
||||
export function X402LeftSection(props: {
|
||||
options: X402PlaygroundOptions;
|
||||
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
|
||||
}) {
|
||||
const { options, setOptions } = props;
|
||||
|
||||
// Local state for chain and token selection
|
||||
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
|
||||
return options.chain?.id;
|
||||
});
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<
|
||||
{ chainId: number; address: string } | undefined
|
||||
>(() => {
|
||||
if (options.tokenAddress && options.chain?.id) {
|
||||
return {
|
||||
address: options.tokenAddress,
|
||||
chainId: options.chain.id,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const chainId = useId();
|
||||
const tokenId = useId();
|
||||
const amountId = useId();
|
||||
const payToId = useId();
|
||||
const waitUntilId = useId();
|
||||
|
||||
const handleChainChange = (chainId: number) => {
|
||||
setSelectedChain(chainId);
|
||||
// Clear token selection when chain changes
|
||||
setSelectedToken(undefined);
|
||||
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
chain: defineChain(chainId),
|
||||
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
|
||||
tokenSymbol: "",
|
||||
tokenDecimals: 18,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTokenChange = (token: TokenMetadata) => {
|
||||
setSelectedToken({
|
||||
address: token.address,
|
||||
chainId: selectedChain!,
|
||||
});
|
||||
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
tokenAddress: token.address as `0x${string}`,
|
||||
tokenSymbol: token.symbol ?? "",
|
||||
tokenDecimals: token.decimals ?? 18,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
amount: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePayToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payTo: e.target.value as `0x${string}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleWaitUntilChange = (
|
||||
value: "simulated" | "submitted" | "confirmed",
|
||||
) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
waitUntil: value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
|
||||
<div className="space-y-4">
|
||||
{/* Chain selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={chainId}>Chain</Label>
|
||||
<BridgeNetworkSelector
|
||||
chainId={selectedChain}
|
||||
onChange={handleChainChange}
|
||||
placeholder="Select a chain"
|
||||
className="bg-card"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Token selection - only show if chain is selected */}
|
||||
{selectedChain && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={tokenId}>Token</Label>
|
||||
<TokenSelector
|
||||
chainId={selectedChain}
|
||||
client={THIRDWEB_CLIENT}
|
||||
enabled={true}
|
||||
onChange={handleTokenChange}
|
||||
placeholder="Select a token"
|
||||
selectedToken={selectedToken}
|
||||
className="bg-card"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Amount input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={amountId}>Amount</Label>
|
||||
<Input
|
||||
id={amountId}
|
||||
type="text"
|
||||
placeholder="0.01"
|
||||
value={options.amount}
|
||||
onChange={handleAmountChange}
|
||||
className="bg-card"
|
||||
/>
|
||||
{options.tokenSymbol && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Amount in {options.tokenSymbol}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pay To input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={payToId}>Pay To Address</Label>
|
||||
<Input
|
||||
id={payToId}
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
value={options.payTo}
|
||||
onChange={handlePayToChange}
|
||||
className="bg-card"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The wallet address that will receive the payment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Wait Until selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={waitUntilId}>Wait Until</Label>
|
||||
<Select
|
||||
value={options.waitUntil}
|
||||
onValueChange={handleWaitUntilChange}
|
||||
>
|
||||
<SelectTrigger className="bg-card">
|
||||
<SelectValue placeholder="Select wait condition" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simulated">Simulated</SelectItem>
|
||||
<SelectItem value="submitted">Submitted</SelectItem>
|
||||
<SelectItem value="confirmed">Confirmed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
When to consider the payment settled: simulated (fastest),
|
||||
submitted (medium), or confirmed (most secure)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { chain, token } from "./constants";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
import { X402LeftSection } from "./X402LeftSection";
|
||||
import { X402RightSection } from "./X402RightSection";
|
||||
|
||||
const defaultOptions: X402PlaygroundOptions = {
|
||||
chain: chain,
|
||||
tokenAddress: token.address as `0x${string}`,
|
||||
tokenSymbol: token.symbol,
|
||||
tokenDecimals: token.decimals,
|
||||
amount: "0.01",
|
||||
payTo: "0x0000000000000000000000000000000000000000",
|
||||
waitUntil: "simulated",
|
||||
};
|
||||
|
||||
export function X402Playground() {
|
||||
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
|
||||
const activeAccount = useActiveAccount();
|
||||
|
||||
// Update payTo address when wallet connects, but only if it's still the default
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
activeAccount?.address &&
|
||||
options.payTo === "0x0000000000000000000000000000000000000000"
|
||||
) {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
payTo: activeAccount.address as `0x${string}`,
|
||||
}));
|
||||
}
|
||||
}, [activeAccount?.address, options.payTo]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
|
||||
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
|
||||
<X402LeftSection options={options} setOptions={setOptions} />
|
||||
</div>
|
||||
<X402RightSection options={options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ConnectButton,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
|
||||
type Tab = "ui" | "client-code" | "server-code";
|
||||
|
||||
export function X402RightSection(props: { options: X402PlaygroundOptions }) {
|
||||
const pathname = usePathname();
|
||||
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
|
||||
return "ui";
|
||||
});
|
||||
|
||||
function setPreviewTab(tab: "ui" | "client-code" | "server-code") {
|
||||
_setPreviewTab(tab);
|
||||
window.history.replaceState({}, "", `${pathname}?tab=${tab}`);
|
||||
}
|
||||
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeAccount = useActiveAccount();
|
||||
|
||||
const paidApiCall = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!activeWallet) {
|
||||
throw new Error("No active wallet");
|
||||
}
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
BigInt(1 * 10 ** 18),
|
||||
);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("chainId", props.options.chain.id.toString());
|
||||
searchParams.set("payTo", props.options.payTo);
|
||||
searchParams.set("amount", props.options.amount);
|
||||
searchParams.set("tokenAddress", props.options.tokenAddress);
|
||||
searchParams.set("decimals", props.options.tokenDecimals.toString());
|
||||
searchParams.set("waitUntil", props.options.waitUntil);
|
||||
|
||||
const url =
|
||||
"/api/paywall" +
|
||||
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
|
||||
const response = await fetchWithPay(url.toString());
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handlePayClick = async () => {
|
||||
paidApiCall.mutate();
|
||||
};
|
||||
|
||||
const clientCode = `import { createThirdwebClient } from "thirdweb";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
|
||||
export default function Page() {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
const onClick = async () => {
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('/api/paid-endpoint');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick}>Pay Now</Button>
|
||||
);
|
||||
}`;
|
||||
|
||||
const serverCode = `// Usage in a Next.js API route
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0xYourServerWalletAddress",
|
||||
waitUtil: "${props.options.waitUntil}",
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
// verify and process the payment
|
||||
const result = await settlePayment({
|
||||
resourceUrl: "https://api.example.com/premium-content",
|
||||
method: "POST",
|
||||
paymentData,
|
||||
payTo: "${props.options.payTo}",
|
||||
network: defineChain(${props.options.chain.id}),
|
||||
price: {
|
||||
amount: "${Number(props.options.amount) * 10 ** props.options.tokenDecimals}",
|
||||
asset: {
|
||||
address: "${props.options.tokenAddress}",
|
||||
},
|
||||
},
|
||||
facilitator: thirdwebFacilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment verified and settled successfully
|
||||
return Response.json({ data: "premium content" });
|
||||
} else {
|
||||
// Payment required
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:w-[764px]">
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
isActive: previewTab === "ui",
|
||||
name: "UI",
|
||||
onClick: () => setPreviewTab("ui"),
|
||||
},
|
||||
{
|
||||
isActive: previewTab === "client-code",
|
||||
name: "Client Code",
|
||||
onClick: () => setPreviewTab("client-code"),
|
||||
},
|
||||
{
|
||||
isActive: previewTab === "server-code",
|
||||
name: "Server Code",
|
||||
onClick: () => setPreviewTab("server-code"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex min-h-[200px] grow justify-center rounded-lg",
|
||||
previewTab === "ui" && "items-center",
|
||||
)}
|
||||
>
|
||||
<BackgroundPattern />
|
||||
|
||||
{previewTab === "ui" && (
|
||||
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
|
||||
<ConnectButton
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={props.options.chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken:
|
||||
props.options.tokenAddress !==
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
? {
|
||||
[props.options.chain.id]: props.options.tokenAddress,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
supportedTokens={
|
||||
props.options.tokenAddress !==
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
? {
|
||||
[props.options.chain.id]: [
|
||||
{
|
||||
address: props.options.tokenAddress,
|
||||
symbol: props.options.tokenSymbol,
|
||||
name: props.options.tokenSymbol,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LockIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">Paid API Call</span>
|
||||
<Badge variant="success">
|
||||
<span className="text-xl font-bold">
|
||||
{props.options.amount}{" "}
|
||||
{props.options.tokenSymbol || "tokens"}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
className="w-full mb-4"
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Access Premium Content
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Pay for access with {props.options.tokenSymbol || "tokens"} on{" "}
|
||||
{props.options.chain.name || `chain ${props.options.chain.id}`}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CodeIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">API Call Response</span>
|
||||
</div>
|
||||
{paidApiCall.isPending && (
|
||||
<div className="text-center">Loading...</div>
|
||||
)}
|
||||
{paidApiCall.isError && (
|
||||
<div className="text-center">
|
||||
Error: {paidApiCall.error.message}
|
||||
</div>
|
||||
)}
|
||||
{paidApiCall.data && (
|
||||
<CodeClient
|
||||
code={JSON.stringify(paidApiCall.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewTab === "client-code" && (
|
||||
<div className="w-full">
|
||||
<CodeClient
|
||||
className="h-full rounded-none border-none"
|
||||
code={clientCode}
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewTab === "server-code" && (
|
||||
<div className="w-full">
|
||||
<CodeClient
|
||||
className="h-full rounded-none border-none"
|
||||
code={serverCode}
|
||||
lang="typescript"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BackgroundPattern() {
|
||||
const color = "hsl(var(--foreground)/15%)";
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-[-1]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButtons(props: {
|
||||
tabs: Array<{
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-start gap-1 rounded-lg border bg-card p-2 shadow-md md:inline-flex">
|
||||
{props.tabs.map((tab) => (
|
||||
<Button
|
||||
className={cn(
|
||||
"gap-2 px-4 text-base",
|
||||
tab.isActive
|
||||
? "bg-accent text-foreground"
|
||||
: "bg-transparent text-muted-foreground",
|
||||
)}
|
||||
key={tab.name}
|
||||
onClick={tab.onClick}
|
||||
variant="ghost"
|
||||
>
|
||||
{tab.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { base } from "thirdweb/chains";
|
||||
|
||||
export const chain = base;
|
||||
export const token = {
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
decimals: 6,
|
||||
};
|
||||
// export const chain = base;
|
||||
// export const token = {
|
||||
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
|
||||
// decimals: 18,
|
||||
// name: "Higher",
|
||||
// symbol: "HIGHER",
|
||||
// version: "1",
|
||||
// };
|
||||
// export const token = {
|
||||
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
|
||||
// decimals: 18,
|
||||
// name: "Stable Coin",
|
||||
// symbol: "SBC",
|
||||
// version: "1",
|
||||
// // primaryType: "Permit",
|
||||
// }
|
||||
// export const chain = defineChain(3338);
|
||||
// export const token = {
|
||||
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
|
||||
// decimals: 6,
|
||||
// name: "USDC",
|
||||
// symbol: "USDC",
|
||||
// version: "2",
|
||||
// }
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import type { Address } from "thirdweb/utils";
|
||||
|
||||
export type X402PlaygroundOptions = {
|
||||
chain: Chain;
|
||||
tokenAddress: Address;
|
||||
tokenSymbol: string;
|
||||
tokenDecimals: number;
|
||||
amount: string;
|
||||
payTo: Address;
|
||||
waitUntil: "simulated" | "submitted" | "confirmed";
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import {
|
||||
ConnectButton,
|
||||
getDefaultToken,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { THIRDWEB_CLIENT } from "../../../../lib/client";
|
||||
|
||||
const chain = arbitrumSepolia;
|
||||
const token = getDefaultToken(chain, "USDC");
|
||||
|
||||
export function X402ClientPreview() {
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeAccount = useActiveAccount();
|
||||
const paidApiCall = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!activeWallet) {
|
||||
throw new Error("No active wallet");
|
||||
}
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
);
|
||||
const response = await fetchWithPay("/api/paywall");
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handlePayClick = async () => {
|
||||
paidApiCall.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
|
||||
<ConnectButton
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken: {
|
||||
[chain.id]: token!.address,
|
||||
},
|
||||
}}
|
||||
supportedTokens={{
|
||||
[chain.id]: [token!],
|
||||
}}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LockIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">Paid API Call</span>
|
||||
<span className="text-xl font-bold text-red-600">$0.01</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
className="w-full mb-4"
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Pay Now
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={"https://faucet.circle.com/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Click here to get USDC on {chain.name}
|
||||
</a>
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CodeIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">API Call Response</span>
|
||||
</div>
|
||||
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
|
||||
{paidApiCall.isError && (
|
||||
<div className="text-center">Error: {paidApiCall.error.message}</div>
|
||||
)}
|
||||
{paidApiCall.data && (
|
||||
<CodeClient
|
||||
code={JSON.stringify(paidApiCall.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { CodeServer } from "@workspace/ui/components/code/code.server";
|
||||
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
|
||||
import { CodeExample, TabName } from "@/components/code/code-example";
|
||||
import { CircleDollarSignIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
import { X402ClientPreview } from "./components/x402-client-preview";
|
||||
import { X402Playground } from "./components/X402Playground";
|
||||
|
||||
const title = "x402 Payments";
|
||||
const description =
|
||||
@@ -30,111 +28,8 @@ export default function Page() {
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
|
||||
>
|
||||
<X402Example />
|
||||
<div className="h-8" />
|
||||
<ServerCodeExample />
|
||||
<X402Playground />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerCodeExample() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h2 className="font-semibold text-xl tracking-tight">
|
||||
Next.js Server Code Example
|
||||
</h2>
|
||||
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
|
||||
Create middleware with the thirdweb facilitator to settle transactions
|
||||
with your server wallet.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
|
||||
<TabName icon={Code2Icon} name="Server Code" />
|
||||
<CodeServer
|
||||
className="h-full rounded-none border-none"
|
||||
code={`// src/middleware.ts
|
||||
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
const thirdwebX402Facilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0xYourWalletAddress",
|
||||
});
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = request.nextUrl.toString();
|
||||
const paymentData = request.headers.get("X-PAYMENT");
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0xYourWalletAddress",
|
||||
network: "eip155:11155111", // or any other chain id
|
||||
price: "$0.01", // can also be a ERC20 token amount
|
||||
facilitator: thirdwebX402Facilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// payment successful, execute the request
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// otherwise, request payment
|
||||
return NextResponse.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid-endpoint"],
|
||||
};
|
||||
|
||||
`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function X402Example() {
|
||||
return (
|
||||
<CodeExample
|
||||
header={{
|
||||
title: "Client Code Example",
|
||||
description:
|
||||
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
|
||||
}}
|
||||
code={`import { createThirdwebClient } from "thirdweb";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
|
||||
export default function Page() {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
const onClick = async () => {
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('/api/paid-endpoint');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick}>Pay Now</Button>
|
||||
);
|
||||
}`}
|
||||
lang="tsx"
|
||||
preview={<X402ClientPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export function RightSection(props: {
|
||||
}
|
||||
auth={connectOptions.enableAuth ? playgroundAuth : undefined}
|
||||
autoConnect={false}
|
||||
className="shadow-xl !max-w-full"
|
||||
className="shadow-xl"
|
||||
client={THIRDWEB_CLIENT}
|
||||
header={{
|
||||
title: connectOptions.modalTitle,
|
||||
|
||||
@@ -49,7 +49,6 @@ export function StyledConnectEmbed(
|
||||
abstract,
|
||||
]}
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="!max-w-full"
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
wallets={WALLETS}
|
||||
{...props}
|
||||
|
||||
@@ -1,42 +1,71 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import { createThirdwebClient, defineChain } from "thirdweb";
|
||||
import { toUnits } from "thirdweb/utils";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { token } from "./app/payments/x402/components/constants";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
|
||||
});
|
||||
|
||||
const chain = arbitrumSepolia;
|
||||
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
|
||||
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
|
||||
const ENGINE_VAULT_ACCESS_TOKEN = process.env
|
||||
.ENGINE_VAULT_ACCESS_TOKEN as string;
|
||||
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
|
||||
const twFacilitator = facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
serverWalletAddress: BACKEND_WALLET_ADDRESS,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
});
|
||||
function createFacilitator(
|
||||
waitUntil: "simulated" | "submitted" | "confirmed" = "simulated",
|
||||
) {
|
||||
return facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
serverWalletAddress: BACKEND_WALLET_ADDRESS,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
waitUtil: waitUntil,
|
||||
});
|
||||
}
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const pathname = request.nextUrl.pathname;
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
|
||||
const paymentData = request.headers.get("X-PAYMENT");
|
||||
const queryParams = request.nextUrl.searchParams;
|
||||
|
||||
const chainId = queryParams.get("chainId");
|
||||
const payTo = queryParams.get("payTo");
|
||||
|
||||
if (!chainId || !payTo) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required parameters" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const amount = queryParams.get("amount") || "0.01";
|
||||
const tokenAddress = queryParams.get("tokenAddress") || token.address;
|
||||
const decimals = queryParams.get("decimals") || token.decimals.toString();
|
||||
const waitUntil =
|
||||
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
|
||||
"simulated";
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
network: `eip155:${chain.id}`,
|
||||
price: "$0.01",
|
||||
payTo: payTo as `0x${string}`,
|
||||
network: defineChain(Number(chainId)),
|
||||
price: {
|
||||
amount: toUnits(amount, parseInt(decimals)).toString(),
|
||||
asset: {
|
||||
address: tokenAddress as `0x${string}`,
|
||||
decimals: decimals ? parseInt(decimals) : token.decimals,
|
||||
},
|
||||
},
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
facilitator: twFacilitator,
|
||||
facilitator: createFacilitator(waitUntil),
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
|
||||
@@ -3,6 +3,16 @@ import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
...tailwindConfig,
|
||||
theme: {
|
||||
...tailwindConfig.theme,
|
||||
extend: {
|
||||
...tailwindConfig.theme?.extend,
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-sans)"],
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
},
|
||||
},
|
||||
},
|
||||
content: [
|
||||
"./src/**/*.{ts,tsx}",
|
||||
// add contents of ui package
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"geist": "^1.5.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
|
||||
@@ -414,7 +414,7 @@ const reactNativeRedirects = {
|
||||
|
||||
const unityRedirects = {
|
||||
// top level
|
||||
"/unity": "/unity/v5",
|
||||
"/unity": "/unity/v6",
|
||||
// blocks
|
||||
"/unity/blocks/getblock": "/unity/v4/blocks/getblock",
|
||||
"/unity/blocks/getblockwithtransactions":
|
||||
|
||||
@@ -213,133 +213,129 @@ export function Header() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="flex w-full flex-col gap-2 border-b bg-background p-4 xl:pb-0 lg:px-8 overflow-hidden">
|
||||
{/* Top row */}
|
||||
<div className="container flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden xl:block">
|
||||
<header className="flex w-full border-b overflow-hidden relative">
|
||||
<div className="container flex-col">
|
||||
{/* Top row */}
|
||||
<div className="flex items-center justify-between gap-6 relative z-10 py-4 lg:pt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden xl:block">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="container hidden items-center justify-between gap-6 xl:flex mt-1">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center py-2 relative px-2.5 hover:text-foreground"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink href={link.href} name={link.name} />
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-violet-700 h-[2px] inset-x-0 rounded-full absolute -bottom-1" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="hidden items-start justify-between gap-6 xl:flex relative z-10">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1.5">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center relative"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
name={link.name}
|
||||
className="py-2.5 px-3 hover:bg-accent rounded-lg hover:text-foreground font-normal"
|
||||
/>
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-foreground h-[2px] inset-x-0 rounded-full absolute -bottom-1.5" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="px-1">
|
||||
<div className="flex items-center">
|
||||
<DropdownLinks
|
||||
category="AI"
|
||||
links={aiLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="SDKs"
|
||||
links={sdkLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="APIs"
|
||||
links={apisLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Tools"
|
||||
links={toolLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Support"
|
||||
links={supportLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
className="px-3 py-2.5 hover:bg-accent hover:text-foreground rounded-lg"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,6 +424,8 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-card/70 backdrop-blur-xl z-0" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -448,31 +446,29 @@ function DropdownLinks(props: {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="inline-flex items-center gap-1 p-0 font-medium text-muted-foreground text-sm hover:bg-transparent hover:text-foreground"
|
||||
className="inline-flex items-center gap-1.5 font-normal text-muted-foreground text-sm hover:bg-accent hover:text-foreground rounded-lg px-3 py-2.5"
|
||||
variant="ghost"
|
||||
>
|
||||
{props.category}
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground opacity-70" />
|
||||
<ChevronDownIcon className="size-3.5 text-muted-foreground opacity-70" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 bg-card p-3"
|
||||
style={{
|
||||
minWidth: "150px",
|
||||
}}
|
||||
className="flex flex-col gap-1 bg-card p-1 rounded-xl min-w-[200px]"
|
||||
sideOffset={14}
|
||||
>
|
||||
{props.links.map((info) => {
|
||||
return (
|
||||
<DropdownMenuItem asChild key={info.name}>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative flex cursor-pointer gap-2 font-medium text-foreground",
|
||||
"relative flex cursor-pointer gap-3 font-medium text-foreground !rounded-lg px-3 py-2",
|
||||
"hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{info.icon && (
|
||||
<info.icon className="size-5 text-foreground" />
|
||||
<info.icon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
@@ -524,6 +520,7 @@ function NavLink(props: {
|
||||
name: string;
|
||||
onClick?: () => void;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -534,6 +531,7 @@ function NavLink(props: {
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground",
|
||||
props.icon ? "flex flex-row gap-3" : "",
|
||||
props.className,
|
||||
)}
|
||||
href={props.href}
|
||||
onClick={props.onClick}
|
||||
|
||||
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 165 KiB |
@@ -0,0 +1,467 @@
|
||||
import {
|
||||
Details,
|
||||
createMetadata,
|
||||
DocImage
|
||||
} from "@doc";
|
||||
import bridgeWidgetDark from "./bridge-widget-dark.png";
|
||||
import bridgeWidgetLight from "./bridge-widget-light.png";
|
||||
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "BridgeWidget Script",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Bridge Widget",
|
||||
description: "Add a widget to add cross swaps and fiat onramp to your app",
|
||||
});
|
||||
|
||||
# BridgeWidget Script
|
||||
|
||||
The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp UI into your app. Just add a script tag to your HTML and get a fully customizable widget — no build setup required.
|
||||
|
||||
## Key Features
|
||||
- Cross-chain token swaps across 85+ blockchains
|
||||
- Fiat onramp support to buy tokens with credit/debit cards
|
||||
- Customizable UI - use prebuilt themes or override with your brand colors
|
||||
- Prefill token selections for a smoother user experience
|
||||
- Display fiat values in multiple currencies
|
||||
- Event callbacks to track user actions (success, error, cancel, disconnect)
|
||||
|
||||
|
||||
<div className='dark-only'>
|
||||
<DocImage src={bridgeWidgetDark} />
|
||||
</div>
|
||||
<div className='light-only'>
|
||||
<DocImage src={bridgeWidgetLight} />
|
||||
</div>
|
||||
|
||||
|
||||
## Script Integration
|
||||
|
||||
```html
|
||||
<!-- Add this script in <head> -->
|
||||
<script src="https://unpkg.com/thirdweb/dist/scripts/bridge-widget.js"></script>
|
||||
|
||||
<!-- Add a container element where you want to render the UI -->
|
||||
<div id="bridge-widget-container"></div>
|
||||
|
||||
<!-- Initialize the widget at the end of <body> -->
|
||||
<script>
|
||||
const container = document.querySelector('#bridge-widget-container');
|
||||
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: "dark",
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
You can customize theme, currencies, token selections, and more. See the full list of options below
|
||||
|
||||
<Details summary="View full type">
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
clientId: string;
|
||||
theme?: "dark" | "light" | {
|
||||
type: "dark" | "light";
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success?: string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
};
|
||||
showThirdwebBranding?: boolean;
|
||||
currency?: SupportedFiatCurrency;
|
||||
swap?: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
buy: {
|
||||
amount: string;
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
purchaseData?: PurchaseData;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
### clientId
|
||||
Set your Project's client ID to identify your project. You create a project for free on [thirdweb dashboard](https://thirdweb.com/team)
|
||||
|
||||
|
||||
### swap
|
||||
The options to configure the "Swap" tab
|
||||
|
||||
```ts
|
||||
type Swap = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="prefill">
|
||||
|
||||
You can prefill Buy and/or Sell tokens for the swap tab. If `tokenAddress` is not provided, the native token will be used. Here are some examples:
|
||||
|
||||
|
||||
#### Set an ERC20 token as the buy token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set a native token as the sell token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base native token (ETH)
|
||||
sellToken: {
|
||||
chainId: 8453,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set amount and token to Buy by default
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set both buy and sell tokens by default
|
||||
|
||||
```ts
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
// Polygon native token (POL)
|
||||
sellToken: {
|
||||
chainId: 137,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="persistTokenSelections">
|
||||
|
||||
Whether to persist the token selections to localStorage so that if the user revisits the widget, the last used tokens are pre-selected.
|
||||
|
||||
The last used tokens do not override the tokens specified in the `prefill` option. By default, it is set to `true`.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onDisconnect">
|
||||
|
||||
Pass a callback to be called when the user disconnects the active wallet.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
### buy
|
||||
|
||||
The options to configure the "Buy" tab UI.
|
||||
|
||||
|
||||
```ts
|
||||
type Buy = {
|
||||
amount: string; // Required
|
||||
chainId: number; // Required
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="amount" tags={["Required"]}>
|
||||
|
||||
Set the default token amount shown in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="chainId" tags={["Required"]}>
|
||||
|
||||
Specify which chain should be selected in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="tokenAddress">
|
||||
|
||||
Specify which token should be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="buttonLabel">
|
||||
|
||||
Override the label for main action button
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="country">
|
||||
|
||||
The user's ISO 3166 alpha-2 country code. This is used to determine onramp provider support.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="presetOptions">
|
||||
|
||||
Preset fiat amounts to display in the UI. Defaults to [5, 10, 20].
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### theme
|
||||
|
||||
Set the theme for the widget. You can either set it to `"dark"`, `"light"` or overrides some or all the colors by passing an object. By default it set to `"dark"`.
|
||||
|
||||
When overriding the colors, you must set the `type` to either `"dark"` or `"light"` so that the widget knows which theme to apply the overrides to.
|
||||
|
||||
|
||||
```ts
|
||||
type Theme = "dark" | "light" | {
|
||||
type: "dark" | "light"; // required
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success? : string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: {
|
||||
type: "dark",
|
||||
colors: {
|
||||
modalBg: "black",
|
||||
primaryText: "white",
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### currency
|
||||
|
||||
The token amounts fiat values will be displayed in this currency. By default, it is set to `"USD"`.
|
||||
|
||||
|
||||
```ts
|
||||
type Currency = "USD" | "EUR" | "GBP" | "JPY" | "KRW" | "CNY" | "INR" | "NOK" | "SEK" | "CHF" | "AUD" | "CAD" | "NZD" | "MXN" | "BRL" | "CLP" | "CZK" | "DKK" | "HKD" | "HUF" | "IDR" | "ILS" | "ISK";
|
||||
```
|
||||
|
||||
|
||||
### showThirdwebBranding
|
||||
|
||||
Whether to show thirdweb branding at the bottom of the widget. By default, it is set to `true`.
|
||||
@@ -102,17 +102,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="react">
|
||||
The quickest way to setup Bridge in your React app is with the [`BuyWidget`](/references/typescript/v5/widgets/BuyWidget) component.
|
||||
|
||||
The quickest way to setup Bridge in your React app is with the [`SwapWidget`](/references/typescript/v5/widgets/SwapWidget) component.
|
||||
|
||||
### Live Playground
|
||||
|
||||
<ArticleIconCard
|
||||
title="Payment Widgets Playground"
|
||||
description="Try out the Payment Widgets in our live playground"
|
||||
title="Swap Widget Playground"
|
||||
description="Try out the Swap Widget in our live playground"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/connect/pay/transactions"
|
||||
href="https://playground.thirdweb.com/bridge/swap-widget"
|
||||
/>
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
Install the thirdweb SDK in your React project:
|
||||
@@ -127,7 +127,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Create a Client
|
||||
|
||||
First, create a client file (e.g., `thirdwebClient.ts`) for reuse throughout your app:
|
||||
First, create a client file for reuse throughout your app:
|
||||
|
||||
```typescript
|
||||
// thirdwebClient.ts
|
||||
@@ -140,7 +140,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Setup the Provider
|
||||
|
||||
Wrap your application with the ThirdwebProvider:
|
||||
Wrap your application with the `ThirdwebProvider`
|
||||
|
||||
```tsx
|
||||
// app.tsx / _app.tsx
|
||||
@@ -155,21 +155,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
}
|
||||
```
|
||||
|
||||
### Buy Widget
|
||||
### Add Swap Widget
|
||||
|
||||
Use the BuyWidget to let users purchase tokens:
|
||||
Use the SwapWidget to let users swap tokens
|
||||
|
||||
```tsx
|
||||
import { BuyWidget } from "thirdweb/react";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { SwapWidget } from "thirdweb/react";
|
||||
|
||||
function PaymentComponent() {
|
||||
function Example() {
|
||||
return (
|
||||
<BuyWidget
|
||||
<SwapWidget
|
||||
client={client}
|
||||
chain={base}
|
||||
amount="10" // 10 USDC
|
||||
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ export const sidebar: SideBar = {
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="container max-w-3xl py-10" data-noindex>
|
||||
<main className="container max-w-4xl py-10" data-noindex>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<div className="changelog-page">
|
||||
<Button asChild>
|
||||
<Button asChild variant="ghost">
|
||||
<Link
|
||||
className="-translate-x-1/4 !p-2 !text-muted-foreground hover:!text-foreground mb-4 bg-transparent"
|
||||
href="/changelog"
|
||||
@@ -64,7 +64,7 @@ export default async function Page(props: {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mb-8 border-t-2" />
|
||||
<div className="mb-8 border-t border-dashed" />
|
||||
|
||||
{ReactHtmlParser(data.html || "", {
|
||||
transform,
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.changelog-page a {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: hsl(var(--muted-foreground));
|
||||
text-decoration-style: dotted;
|
||||
color: hsl(var(--foreground));
|
||||
text-underline-offset: 5px;
|
||||
transition: color 200ms ease;
|
||||
}
|
||||
|
||||
.changelog-page h2 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@ export function Author(props: { name: string; profileImage?: string | null }) {
|
||||
{props.profileImage && (
|
||||
<img
|
||||
alt=""
|
||||
className="size-8 rounded-[50%] border"
|
||||
className="size-6 rounded-[50%] border"
|
||||
src={props.profileImage}
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium text-muted-foreground text-sm">
|
||||
{props.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">{props.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TableOfContentsSideBar } from "../../../components/others/TableOfContents";
|
||||
|
||||
const onlyShowPostTitle = (h: HTMLHeadingElement) => {
|
||||
return h.classList.contains("changelog-title");
|
||||
};
|
||||
|
||||
export function ChangelogIndexTOC() {
|
||||
return <TableOfContentsSideBar filterHeading={onlyShowPostTitle} />;
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
import { formatDistance } from "date-fns";
|
||||
import { formatDate } from "date-fns";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function RenderDate(props: { iso: string; className?: string }) {
|
||||
return (
|
||||
<time
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
props.className,
|
||||
)}
|
||||
dateTime={props.iso}
|
||||
>
|
||||
{formatDistance(new Date(props.iso), new Date())} ago
|
||||
<time className={cn("text-sm", props.className)} dateTime={props.iso}>
|
||||
{formatDate(new Date(props.iso), "MMM d, yyyy")}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { createMetadata } from "@doc";
|
||||
import GithubSlugger from "github-slugger";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import ReactHtmlParser from "react-html-parser";
|
||||
import { Heading } from "@/components/Document";
|
||||
import { Author } from "./components/Author";
|
||||
import { ChangelogIndexTOC } from "./components/ChangeLogIndexTOC";
|
||||
import { RenderDate } from "./components/RenderData";
|
||||
import { fetchChangeLogs, fetchPost } from "./ghost";
|
||||
import { transform } from "./utils/transform";
|
||||
@@ -23,12 +20,15 @@ export const metadata = createMetadata({
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<main className="container" data-noindex>
|
||||
<div className="flex justify-between gap-16">
|
||||
<div className="w-full max-w-[850px] grow-0 ">
|
||||
<PageContent />
|
||||
<main data-noindex>
|
||||
<div className="border-b py-12 border-dashed">
|
||||
<div className="container">
|
||||
<h1 className="font-semibold text-4xl tracking-tight">Changelog</h1>
|
||||
</div>
|
||||
<ChangelogIndexTOC />
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<PageContent />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -39,53 +39,48 @@ async function PageContent() {
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
return (
|
||||
<div className="changelog-page py-6">
|
||||
<h1 className="mb-10 font-semibold text-3xl tracking-tighter">
|
||||
Changelog
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-col gap-10 xl:border-l-2 xl:pl-12">
|
||||
<div className="changelog-page overflow-hidden">
|
||||
<div className="flex flex-col gap-10 xl:border-l border-dashed xl:pl-16 max-w-4xl ml-auto pb-20">
|
||||
{posts.map((post) => {
|
||||
return (
|
||||
<div className="relative pb-10" key={post.id}>
|
||||
<div className="mb-2 flex items-center gap-5">
|
||||
<div className="relative pt-10" key={post.id}>
|
||||
<div className="-left-16 -translate-x-full absolute top-12 hidden xl:flex gap-5 ml-3 items-center">
|
||||
{post.published_at && (
|
||||
<RenderDate className="text-base" iso={post.published_at} />
|
||||
<div className="hidden xl:block text-sm text-muted-foreground">
|
||||
<RenderDate iso={post.published_at} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-5">
|
||||
{post.authors?.map((author) => {
|
||||
return (
|
||||
<Author
|
||||
key={author.id}
|
||||
name={author.name || ""}
|
||||
profileImage={author.profile_image}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="size-6 rounded-[50%] border bg-background flex items-center justify-center">
|
||||
<div className="size-2 bg-blue-500 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="-left-12 -translate-x-1/2 absolute top-12 hidden size-7 items-center justify-center rounded-[50%] bg-foreground md:size-10 xl:flex">
|
||||
<PlusIcon className="size-6 text-background" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Heading
|
||||
anchorClassName="[&>a]:hidden m-0 border-b pb-5 mb-5"
|
||||
anchorId={slugger.slug(post.title || "")}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="changelog-title"
|
||||
level={2}
|
||||
>
|
||||
<Link
|
||||
className="!text-foreground font-bold text-3xl tracking-tighter hover:underline md:text-4xl xl:text-5xl"
|
||||
href={`/changelog/${post.slug}`}
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
{post.published_at && (
|
||||
<div className="xl:hidden inline-flex rounded-full border px-3 py-2 mb-4 text-muted-foreground text-xs bg-card">
|
||||
<RenderDate iso={post.published_at} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RenderChangelogContent slug={post.slug} />
|
||||
<div className="space-y-3 mb-5">
|
||||
<Heading
|
||||
anchorClassName="[&>a]:hidden m-0"
|
||||
anchorId={slugger.slug(post.title || "")}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="changelog-title"
|
||||
level={2}
|
||||
>
|
||||
<Link
|
||||
className="!text-foreground font-semibold text-3xl md:text-4xl tracking-tight hover:underline !leading-tight"
|
||||
href={`/changelog/${post.slug}`}
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
<RenderChangelogContent slug={post.slug} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export const transform: Transform = (node, index: number) => {
|
||||
node.attribs.class = "text-center text-sm text-muted-foreground";
|
||||
return;
|
||||
}
|
||||
return <Paragraph>{getChildren()}</Paragraph>;
|
||||
return <Paragraph className="leading-7">{getChildren()}</Paragraph>;
|
||||
}
|
||||
|
||||
if (node.name === "ul") {
|
||||
@@ -77,7 +77,7 @@ export const transform: Transform = (node, index: number) => {
|
||||
}
|
||||
node.attribs.class = "rounded-lg";
|
||||
return (
|
||||
<div className="my-5 flex justify-center rounded-lg border p-4">
|
||||
<div className="my-6 flex justify-center rounded-lg border border-dashed p-2 bg-card/50">
|
||||
{convertNodeToElement(node, index, transform)}
|
||||
</div>
|
||||
);
|
||||
@@ -109,7 +109,13 @@ export const transform: Transform = (node, index: number) => {
|
||||
// }
|
||||
}
|
||||
if (code) {
|
||||
return <CodeBlock code={code} lang={lang} />;
|
||||
return (
|
||||
<CodeBlock
|
||||
code={code}
|
||||
lang={lang}
|
||||
className="bg-card/50 border-dashed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebApi | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Low-level interaction with the Thirdweb API for enhanced SDK functionality and extensions.",
|
||||
});
|
||||
|
||||
# [ThirdwebApi](/reference) .NET Integration
|
||||
`ThirdwebApi` is the low-level HTTP client that powers the higher-level Thirdweb .NET SDK. Reach for it when you need to call https://api.thirdweb.com directly, build custom extensions, or inspect raw API payloads. The client works everywhere .NET runs, including Unity and MAUI.
|
||||
|
||||
## What you unlock with ThirdwebApi
|
||||
|
||||
| Capability | Why teams use it |
|
||||
| --- | --- |
|
||||
| **Authentication** | Orchestrate SMS/email codes, SIWE, OAuth redirects, and passkeys from your own backend before handing off JWTs to wallets. |
|
||||
| **Wallet services** | Look up user/server wallets, enumerate tokens/NFTs, stream transaction history, sign messages, or send value programmatically. |
|
||||
| **Smart contracts** | Batch read/write invocations, fetch metadata & signatures, deploy bytecode, and capture emitted events with pagination. |
|
||||
| **Transactions pipeline** | Queue, monitor, and introspect Engine-backed transactions with execution metadata and status tracking. |
|
||||
| **Tokens & bridge** | Launch ERC20s, list circulating assets, price fiat ↔ crypto, or bridge/swap liquidity across chains. |
|
||||
| **Payments & x402** | Spin up hosted purchases, reconcile payment history, and prepare X402 machine-payable requests. |
|
||||
| **AI assistant** | Embed natural-language orchestration that can read state, prepare contract calls, swap assets, or route transactions for end-users.
|
||||
|
||||
> **Backend first** — All of the above accept your project secret via `x-secret-key`. Client-side use cases can opt into `x-client-id` + user JWT flows where supported.
|
||||
|
||||
## When to reach for ThirdwebApi
|
||||
- You need endpoints that aren't yet wrapped by the higher-level SDK helpers.
|
||||
- You're building backend services that must authenticate with a Thirdweb secret key.
|
||||
- You want full visibility into contract metadata, ABI definitions, and compilation artifacts.
|
||||
- You plan to orchestrate auth, payments, or cross-chain flows without managing disparate providers.
|
||||
|
||||
## Quickstart workflow
|
||||
|
||||
### Instantiate an authenticated client
|
||||
Create a reusable `ThirdwebClient` instance with your backend secret key. This unlocks every raw API surface exposed by Thirdweb.
|
||||
|
||||
```csharp
|
||||
// Program.cs / dependency container registration
|
||||
var client = ThirdwebClient.Create(secretKey: Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"));
|
||||
```
|
||||
|
||||
### Example operation
|
||||
Contract metadata is the canonical example for low-level reads. The same pattern applies to other namespaces like `/v1/wallets/*`, `/v1/payments/*`, or `/ai/chat`—swap the method call but keep your authenticated client.
|
||||
|
||||
```csharp
|
||||
// using Newtonsoft.Json;
|
||||
|
||||
var metadata = await client.Api.GetContractMetadataAsync(
|
||||
chainId: 1,
|
||||
address: "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8"
|
||||
);
|
||||
|
||||
Console.WriteLine($"ABI: {JsonConvert.SerializeObject(metadata.Result.Output.Abi, Formatting.Indented)}");
|
||||
Console.WriteLine($"Compiler version: {metadata.Result.Compiler.Version}");
|
||||
```
|
||||
@@ -36,8 +36,4 @@ var calldata = contract.CreateCallData("myFunction", param1, param2);
|
||||
|
||||
## Available Contract Extensions
|
||||
|
||||
Please refer to the `ThirdwebExtensions` [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebExtensions.html) for a complete list of available contract extensions.
|
||||
|
||||
If you are using our [Marketplace](https://thirdweb.com/thirdweb.eth/MarketplaceV3) contract, check out our [Marketplace-specific](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebMarketplaceExtensions.html) extensions.
|
||||
|
||||
|
||||
Please refer to the `ThirdwebExtensions` [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebExtensions.html) for a complete list of available contract extensions.
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebContract.Prepare | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate a low level Transaction object from a contract prepare call.",
|
||||
});
|
||||
|
||||
# ThirdwebContract.Prepare
|
||||
|
||||
Instantiate a low level [Transaction](/dotnet/transactions/create) object from a contract prepare call.
|
||||
|
||||
Useful for preparing, simulating and manipulating transactions before sending them to the blockchain.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
ThirdwebTransaction transaction = await contract.Prepare(wallet, contract, "methodName", weiValue, parameters);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### wallet (required)
|
||||
|
||||
An instance of `IThirdwebWallet`. This represents the signer of the transaction, which can be any type of wallet provider.
|
||||
|
||||
### contract (required)
|
||||
|
||||
An instance of `ThirdwebContract`. Represents the smart contract you wish to interact with.
|
||||
|
||||
### methodName (required)
|
||||
|
||||
The name of the smart contract method you intend to call. Must be a `string`.
|
||||
|
||||
### weiValue (required)
|
||||
|
||||
The amount of Wei to send with payable transactions as a `BigInteger`. This is often set to 0 for methods that don't require sending Ether.
|
||||
|
||||
### parameters (optional)
|
||||
|
||||
The parameters to pass to the smart contract method, if any. Provide these as a comma-separated list after weiValue.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### Transaction
|
||||
|
||||
Returns an instance of `ThirdwebTransaction` configured with the provided parameters, ready to be sent to the blockchain.
|
||||
|
||||
</Details>
|
||||
@@ -76,16 +76,16 @@ Check the SDK documentation for more advanced features like interacting with sma
|
||||
|
||||
<Stack>
|
||||
|
||||
<ArticleIconCard
|
||||
href="/unity"
|
||||
icon={GraduationCap}
|
||||
title="Unity Integration"
|
||||
/>
|
||||
|
||||
<ArticleIconCard
|
||||
href="/dotnet/godot"
|
||||
icon={GraduationCap}
|
||||
title="Godot Integration"
|
||||
/>
|
||||
|
||||
<ArticleIconCard
|
||||
href="/unity/v5"
|
||||
icon={GraduationCap}
|
||||
title="Unity Integration"
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
|
||||
@@ -24,7 +24,7 @@ To add the Thirdweb .NET SDK to your Godot project, you will need to manually ed
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<!-- Other NuGet package references -->
|
||||
<PackageReference Include="Thirdweb" Version="2.4.0" />
|
||||
<PackageReference Include="Thirdweb" Version="3.0.0" />
|
||||
<!-- Update with the latest version when necessary -->
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebInsight | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate Insight to fetch all the blockchain data you can fathom.",
|
||||
});
|
||||
|
||||
# [Insight](https://thirdweb.com/insight) Indexer .NET Integration
|
||||
Insight is an extremely useful and performant tool to query blockchain data and can decorate it quite nicely too.
|
||||
|
||||
Did you know that vitalik, on Ethereum, Polygon and Arbitrum alone owns 7811 ERC20s, 34,362 ERC721s and 2,818 ERC1155s?
|
||||
Let's go through some examples!
|
||||
|
||||
### Instantiation
|
||||
```csharp
|
||||
using Thirdweb.Indexer;
|
||||
|
||||
// Create a ThirdwebInsight instance
|
||||
var insight = await ThirdwebInsight.Create(client);
|
||||
```
|
||||
|
||||
### Simple Filters
|
||||
```csharp
|
||||
// Setup some filters
|
||||
var address = await Utils.GetAddressFromENS(client, "vitalik.eth");
|
||||
var chains = new BigInteger[] { 1, 137, 42161 };
|
||||
```
|
||||
|
||||
### Fetching all tokens owned by a given address
|
||||
```csharp
|
||||
// Fetch all token types
|
||||
var tokens = await insight.GetTokens(address, chains);
|
||||
Console.WriteLine($"ERC20 Count: {tokens.erc20Tokens.Length} | ERC721 Count: {tokens.erc721Tokens.Length} | ERC1155 Count: {tokens.erc1155Tokens.Length}");
|
||||
```
|
||||
|
||||
### Fetching a specific type of token owned by a given address
|
||||
```csharp
|
||||
// Fetch ERC20s only
|
||||
var erc20Tokens = await insight.GetTokens_ERC20(address, chains);
|
||||
Console.WriteLine($"ERC20 Tokens: {JsonConvert.SerializeObject(erc20Tokens, Formatting.Indented)}");
|
||||
|
||||
// Fetch ERC721s only
|
||||
var erc721Tokens = await insight.GetTokens_ERC721(address, chains);
|
||||
Console.WriteLine($"ERC721 Tokens: {JsonConvert.SerializeObject(erc721Tokens, Formatting.Indented)}");
|
||||
|
||||
// Fetch ERC1155s only
|
||||
var erc1155Tokens = await insight.GetTokens_ERC1155(address, chains);
|
||||
Console.WriteLine($"ERC1155 Tokens: {JsonConvert.SerializeObject(erc1155Tokens, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
### Fetching Events
|
||||
Note: most of these parameters are optional (and some are not showcased here)
|
||||
```csharp
|
||||
// Fetch events (great amount of optional filters available)
|
||||
var events = await insight.GetEvents(
|
||||
chainIds: new BigInteger[] { 1 }, // ethereum
|
||||
contractAddress: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", // bored apes
|
||||
eventSignature: "Transfer(address,address,uint256)", // transfer event
|
||||
fromTimestamp: Utils.GetUnixTimeStampNow() - 3600, // last hour
|
||||
sortBy: SortBy.TransactionIndex, // block number, block timestamp or transaction index
|
||||
sortOrder: SortOrder.Desc, // latest first
|
||||
limit: 5 // last 5 transfers
|
||||
);
|
||||
Console.WriteLine($"Events: {JsonConvert.SerializeObject(events, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
### Fetching Transactions
|
||||
```csharp
|
||||
// Fetch transactions (great amount of optional filters available)
|
||||
var transactions = await insight.GetTransactions(
|
||||
chainIds: new BigInteger[] { 1 }, // ethereum
|
||||
contractAddress: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", // bored apes
|
||||
fromTimestamp: Utils.GetUnixTimeStampNow() - 3600, // last hour
|
||||
sortBy: SortBy.TransactionIndex, // block number, block timestamp or transaction index
|
||||
sortOrder: SortOrder.Desc, // latest first
|
||||
limit: 5 // last 5 transactions
|
||||
);
|
||||
Console.WriteLine($"Transactions: {JsonConvert.SerializeObject(transactions, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
### Fetching NFTs with Metadata
|
||||
Insight can return rich NFT Metadata. Extensions can turn Insight results into familiar NFT objects.
|
||||
|
||||
```csharp
|
||||
// Fetch ERC721s with extra metadata returned
|
||||
var erc721Tokens = await insight.GetTokens_ERC721(address, chains, withMetadata: true);
|
||||
|
||||
// Use ToNFT or ToNFTList extensions
|
||||
var convertedNft = erc721Tokens[0].ToNFT();
|
||||
var convertedNfts = erc721Tokens.ToNFTList();
|
||||
|
||||
// Use NFT Extensions (GetNFTImageBytes, or GetNFTSprite in Unity)
|
||||
var imageBytes = await convertedNft.GetNFTImageBytes(client);
|
||||
var pathToSave = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "nft.png");
|
||||
await File.WriteAllBytesAsync(pathToSave, imageBytes);
|
||||
Console.WriteLine($"NFT image saved to: {pathToSave}");
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebNebula | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate Nebula to interact with a blockchain-powered AI assistant.",
|
||||
});
|
||||
|
||||
# [Nebula AI](https://thirdweb.com/nebula) .NET Integration
|
||||
The last piece of the puzzle required to create .NET apps and games leveraging a blockchain-powered AI assistant or NPC that also has the power to fully execute transactions.
|
||||
|
||||
## Read, Write, Reason.
|
||||
The best way to understand is to look at examples.
|
||||
|
||||
We'll prepare some context for the AI - here, we'll generate a basic `PrivateKeyWallet` and upgrade it to a `SmartWallet` on Sepolia.
|
||||
```csharp
|
||||
// Prepare some context
|
||||
var myChain = 11155111; // Sepolia
|
||||
var mySigner = await PrivateKeyWallet.Generate(client);
|
||||
var myWallet = await SmartWallet.Create(mySigner, myChain);
|
||||
var myContractAddress = "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8"; // DropERC1155
|
||||
var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; // Sepolia USDC
|
||||
```
|
||||
|
||||
A one liner creates a Nebula session.
|
||||
```csharp
|
||||
// Create a Nebula session
|
||||
var nebula = await ThirdwebNebula.Create(client);
|
||||
```
|
||||
|
||||
You can **Chat** with Nebula. The Chat method accepts any `IThirdwebWallet` as an optional parameter, context will automatically be updated.
|
||||
```csharp
|
||||
// Chat, passing wallet context
|
||||
var response1 = await nebula.Chat(message: "What is my wallet address?", wallet: myWallet);
|
||||
Console.WriteLine($"Response 1: {response1.Message}");
|
||||
```
|
||||
|
||||
You may also pass it smart contract context.
|
||||
```csharp
|
||||
// Chat, passing contract context
|
||||
var response2 = await nebula.Chat(
|
||||
message: "What's the total supply of token id 0 for this contract?",
|
||||
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
|
||||
);
|
||||
Console.WriteLine($"Response 2: {response2.Message}");
|
||||
```
|
||||
|
||||
You can have a full on conversation with it with our **Chat** override accepting multiple messages.
|
||||
```csharp
|
||||
// Chat, passing multiple messages and context
|
||||
var response3 = await nebula.Chat(
|
||||
messages: new List<NebulaChatMessage>
|
||||
{
|
||||
new($"Tell me the name of this contract: {myContractAddress}", NebulaChatRole.User),
|
||||
new("The name of the contract is CatDrop", NebulaChatRole.Assistant),
|
||||
new("What's the symbol of this contract?", NebulaChatRole.User),
|
||||
},
|
||||
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
|
||||
);
|
||||
Console.WriteLine($"Response 3: {response3.Message}");
|
||||
```
|
||||
|
||||
Chatting is cool, but what if I just want it to _**do**_ things and stop talking so much?
|
||||
|
||||
You can **Execute** transactions directly with a simple prompt.
|
||||
```csharp
|
||||
// Execute, this directly sends transactions
|
||||
var executionResult = await nebula.Execute("Approve 1 USDC to vitalik.eth", wallet: myWallet, context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress }));
|
||||
if (executionResult.TransactionReceipts != null && executionResult.TransactionReceipts.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Receipt: {executionResult.TransactionReceipts[0]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Message: {executionResult.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, **Execute** can take in multiple messages.
|
||||
```csharp
|
||||
// Batch execute
|
||||
var batchExecutionResult = await nebula.Execute(
|
||||
new List<NebulaChatMessage>
|
||||
{
|
||||
new("What's the address of vitalik.eth", NebulaChatRole.User),
|
||||
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
|
||||
new("Approve 1 USDC to them", NebulaChatRole.User),
|
||||
},
|
||||
wallet: myWallet,
|
||||
context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress })
|
||||
);
|
||||
if (batchExecutionResult.TransactionReceipts != null && batchExecutionResult.TransactionReceipts.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Receipts: {JsonConvert.SerializeObject(batchExecutionResult.TransactionReceipts, Formatting.Indented)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Message: {batchExecutionResult.Message}");
|
||||
}
|
||||
```
|
||||
@@ -2,35 +2,6 @@ import { CodeIcon, ExternalLinkIcon, ZapIcon } from "lucide-react";
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import type { SidebarLink } from "@/components/others/Sidebar";
|
||||
|
||||
const walletProviders: SidebarLink = (() => {
|
||||
const parentSlug = "/dotnet/wallets/providers";
|
||||
return {
|
||||
links: [
|
||||
{
|
||||
href: `${parentSlug}/in-app-wallet`,
|
||||
name: "In App Wallet",
|
||||
},
|
||||
{
|
||||
href: `${parentSlug}/ecosystem-wallet`,
|
||||
name: "Ecosystem Wallet",
|
||||
},
|
||||
{
|
||||
href: `${parentSlug}/account-abstraction`,
|
||||
name: "Account Abstraction",
|
||||
},
|
||||
{
|
||||
href: `${parentSlug}/private-key`,
|
||||
name: "Private Key Wallet",
|
||||
},
|
||||
{
|
||||
href: `${parentSlug}/engine-wallet`,
|
||||
name: "Engine Wallet",
|
||||
},
|
||||
],
|
||||
name: "Wallet Providers",
|
||||
};
|
||||
})();
|
||||
|
||||
const walletActions: SidebarLink = (() => {
|
||||
const parentSlug = "/dotnet/wallets/actions";
|
||||
return {
|
||||
@@ -179,15 +150,12 @@ const walletActions: SidebarLink = (() => {
|
||||
})();
|
||||
|
||||
const contracts: SidebarLink = {
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/dotnet/contracts/create",
|
||||
name: "Create Contract",
|
||||
},
|
||||
{
|
||||
href: "/dotnet/contracts/extensions",
|
||||
name: "Contract Extensions",
|
||||
},
|
||||
{
|
||||
href: "/dotnet/contracts/read",
|
||||
name: "Read Contract",
|
||||
@@ -197,15 +165,20 @@ const contracts: SidebarLink = {
|
||||
name: "Write Contract",
|
||||
},
|
||||
{
|
||||
href: "/dotnet/contracts/prepare",
|
||||
name: "Prepare Transaction",
|
||||
href: "/dotnet/contracts/extensions",
|
||||
name: "Contract Extensions",
|
||||
},
|
||||
],
|
||||
name: "Contract Interactions",
|
||||
name: "Contracts",
|
||||
};
|
||||
|
||||
const transactions: SidebarLink = {
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/dotnet/transactions/prepare",
|
||||
name: "Prepare Contract Transaction",
|
||||
},
|
||||
{
|
||||
href: "/dotnet/transactions/create",
|
||||
name: "Create Transaction",
|
||||
@@ -219,7 +192,7 @@ const transactions: SidebarLink = {
|
||||
name: "Static Methods",
|
||||
},
|
||||
],
|
||||
name: "Transaction Builder",
|
||||
name: "Transactions",
|
||||
};
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
@@ -245,7 +218,7 @@ export const sidebar: SideBar = {
|
||||
isCollapsible: true,
|
||||
links: [
|
||||
{
|
||||
href: "/unity/v5",
|
||||
href: "/unity",
|
||||
name: "Unity",
|
||||
},
|
||||
{
|
||||
@@ -285,67 +258,41 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [walletProviders, walletActions],
|
||||
links: [
|
||||
{
|
||||
href: `/dotnet/wallets/user-wallet`,
|
||||
name: "User Wallet",
|
||||
},
|
||||
{
|
||||
href: `/dotnet/wallets/server-wallet`,
|
||||
name: "Server Wallet",
|
||||
},
|
||||
{
|
||||
href: `/dotnet/wallets/account-abstraction`,
|
||||
name: "Account Abstraction",
|
||||
},
|
||||
walletActions,
|
||||
],
|
||||
name: "Wallets",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [contracts, transactions],
|
||||
name: "Transactions",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
contracts,
|
||||
transactions,
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/dotnet/universal-bridge/quickstart",
|
||||
href: "/dotnet/api/quickstart",
|
||||
name: "Quickstart",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Bridge.ThirdwebBridge.html",
|
||||
name: "Full Reference",
|
||||
href: "/reference",
|
||||
name: "API Full Reference",
|
||||
},
|
||||
],
|
||||
name: "Payments",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/dotnet/insight/quickstart",
|
||||
name: "Quickstart",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Indexer.ThirdwebInsight.html",
|
||||
name: "Insight Full Reference",
|
||||
},
|
||||
],
|
||||
name: "Indexer",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/dotnet/nebula/quickstart",
|
||||
name: "Quickstart",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.AI.ThirdwebNebula.html",
|
||||
name: "Nebula Full Reference",
|
||||
},
|
||||
],
|
||||
name: "AI",
|
||||
name: "API",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
|
||||
@@ -8,9 +8,10 @@ export const metadata = createMetadata({
|
||||
|
||||
# ThirdwebTransaction.Create
|
||||
|
||||
<Callout variant="warning" title="Advanced Use Case">
|
||||
If you are interacting with a contract, it is best to use
|
||||
[ThirdwebContract.Prepare](/dotnet/contracts/prepare) to create a transaction.
|
||||
<Callout variant="warning" title="Preferred Workflow">
|
||||
When you're starting from a contract method, use
|
||||
[ThirdwebContract.Prepare](/dotnet/transactions/prepare) to build the transaction
|
||||
before customizing it here.
|
||||
</Callout>
|
||||
|
||||
The `ThirdwebTransaction.Create` method allows advanced users to manually create transactions using specific wallet credentials and transaction inputs. This method is ideal for users who need precise control over the transaction details.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebContract.Prepare | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Generate a ThirdwebTransaction from a contract call before customizing or sending it.",
|
||||
});
|
||||
|
||||
# ThirdwebContract.Prepare
|
||||
|
||||
`ThirdwebContract.Prepare` bridges contract interactions and the transaction tooling within the Thirdweb .NET SDK. Use it when you want to start from a contract method, but need fine-grained control over the resulting `ThirdwebTransaction` before dispatching it.
|
||||
|
||||
<Callout type="info">
|
||||
Looking for the contract APIs themselves? Check out the [Contracts guides](/dotnet/contracts/create) for creating, reading, and writing before you promote a call into a transaction.
|
||||
</Callout>
|
||||
|
||||
## Why prepare first?
|
||||
|
||||
- **Simulation & inspection** – Prepare the transaction and run [`ThirdwebTransaction.Simulate`](/dotnet/transactions/static) before sending.
|
||||
- **Customization** – Chain the [`ThirdwebTransaction` instance methods](/dotnet/transactions/instance) to adjust calldata, value, gas, or paymaster options.
|
||||
- **Wallet-agnostic** – Works with any `IThirdwebWallet`, letting you bridge user wallets, smart wallets, or server wallets into the same flow.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
ThirdwebTransaction transaction = await contract.Prepare(
|
||||
wallet,
|
||||
contract,
|
||||
"methodName",
|
||||
weiValue,
|
||||
parameters
|
||||
);
|
||||
```
|
||||
|
||||
Once prepared, you can tweak or send the transaction:
|
||||
|
||||
```csharp
|
||||
// Optional: override calldata, value, or other fields
|
||||
transaction = transaction
|
||||
.SetValue(BigInteger.Zero)
|
||||
.SetGasLimit(250000);
|
||||
|
||||
// Optional: simulate or estimate costs
|
||||
await ThirdwebTransaction.Simulate(transaction);
|
||||
var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(transaction);
|
||||
|
||||
// Finally: send the transaction and wait for the receipt
|
||||
TransactionReceipt receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
<Details summary="Prepare parameters">
|
||||
|
||||
### wallet (required)
|
||||
|
||||
An instance of `IThirdwebWallet`. This signer provides the context for populating sender-specific defaults and will ultimately authorize the transaction.
|
||||
|
||||
### contract (required)
|
||||
|
||||
An instance of `ThirdwebContract`. Represents the smart contract whose method you're calling.
|
||||
|
||||
### methodName (required)
|
||||
|
||||
The name of the contract method to invoke. Must be a `string`.
|
||||
|
||||
### weiValue (required)
|
||||
|
||||
The amount of Wei to send with the transaction, as a `BigInteger`. Use `BigInteger.Zero` when no value transfer is needed.
|
||||
|
||||
### parameters (optional)
|
||||
|
||||
The arguments to forward to the contract method. Provide them in the same order as the ABI definition.
|
||||
|
||||
</Details>
|
||||
|
||||
## Return value
|
||||
|
||||
<Details summary="ThirdwebTransaction">
|
||||
|
||||
Returns a fully populated `ThirdwebTransaction` that mirrors the contract invocation. You can send it immediately or pipe it through additional configuration helpers before execution.
|
||||
|
||||
</Details>
|
||||
@@ -1,171 +0,0 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Payments | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Enable your users to trade assets across any supported chain using any token.",
|
||||
});
|
||||
|
||||
# Payments .NET Integration
|
||||
|
||||
Payments enables your users to trade assets across any supported chain using any token.
|
||||
|
||||
To streamline onchain asset trading, we've added .NET extensions that work seamlessly with any `IThirdwebWallet` nicely.
|
||||
|
||||
## Core APIs
|
||||
|
||||
The design is akin to letting us know what your intent is.
|
||||
- Buy: "I want to buy x USDC on y Chain using z Token"
|
||||
- Sell: "I want to sell x USDC on y Chain for z Token"
|
||||
- Transfer: "Just transfer all my money to vitalik"
|
||||
- Onramp: "I want to buy x USDC on y Chain by paying with card"
|
||||
|
||||
We will return the transactions needed to achieve whatever you desire.
|
||||
You may then handle execution yourself or use our extensions.
|
||||
|
||||
### Instantiation
|
||||
```csharp
|
||||
using Thirdweb.Bridge;
|
||||
|
||||
// Create a ThirdwebBridge instance
|
||||
var bridge = await ThirdwebBridge.Create(client);
|
||||
```
|
||||
|
||||
### Buy - Get a quote for buying a specific amount of tokens
|
||||
```csharp
|
||||
var buyQuote = await bridge.Buy_Quote(
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
|
||||
destinationChainId: 324,
|
||||
destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
|
||||
buyAmountWei: BigInteger.Parse("0.1".ToWei())
|
||||
);
|
||||
Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
### Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens
|
||||
```csharp
|
||||
var preparedBuy = await bridge.Buy_Prepare(
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
|
||||
destinationChainId: 324,
|
||||
destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
|
||||
buyAmountWei: BigInteger.Parse("0.1".ToWei()),
|
||||
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
|
||||
receiver: await myWallet.GetAddress()
|
||||
);
|
||||
Console.WriteLine($"Prepared Buy contains {preparedBuy.Transactions.Count} transaction(s)!");
|
||||
```
|
||||
|
||||
### Sell - Get a quote for selling a specific amount of tokens
|
||||
```csharp
|
||||
var sellQuote = await bridge.Sell_Quote(
|
||||
originChainId: 324,
|
||||
originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
|
||||
destinationChainId: 1,
|
||||
destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
|
||||
sellAmountWei: BigInteger.Parse("0.1".ToWei())
|
||||
);
|
||||
Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
### Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens
|
||||
```csharp
|
||||
var preparedSell = await bridge.Sell_Prepare(
|
||||
originChainId: 324,
|
||||
originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
|
||||
destinationChainId: 1,
|
||||
destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
|
||||
sellAmountWei: BigInteger.Parse("0.1".ToWei()),
|
||||
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
|
||||
receiver: await myWallet.GetAddress()
|
||||
);
|
||||
Console.WriteLine($"Prepared Sell contains {preparedSell.Transactions.Count} transaction(s)!");
|
||||
```
|
||||
|
||||
### Transfer - Get an executable transaction for transferring a specific amount of tokens
|
||||
_Why not just transfer with the SDK? Stay tuned for webhooks, think direct payments!_
|
||||
```csharp
|
||||
var preparedTransfer = await bridge.Transfer_Prepare(
|
||||
chainId: 137,
|
||||
tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
|
||||
transferAmountWei: BigInteger.Parse("0.1".ToWei()),
|
||||
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
|
||||
receiver: await myWallet.GetAddress()
|
||||
);
|
||||
Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
## Manual Execution
|
||||
_This is not production code, we're just showcasing some of the APIs that would help you execute and poll status here._
|
||||
```csharp
|
||||
// You may use our extensions to execute yourself...
|
||||
var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet);
|
||||
var myHash = await ThirdwebTransaction.Send(myTx);
|
||||
|
||||
// ...and poll for the status...
|
||||
var status = await bridge.Status(transactionHash: myHash, chainId: 1);
|
||||
var isComplete = status.StatusType == StatusType.COMPLETED;
|
||||
Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}");
|
||||
|
||||
// Or use our Execute extensions directly to handle everything for you!
|
||||
```
|
||||
|
||||
## Managed Execution
|
||||
The SDK comes with some extensions that simplify the use of `ThirdwebBridge`.
|
||||
|
||||
```csharp
|
||||
// Execute a prepared Buy
|
||||
var buyResult = await bridge.Execute(myWallet, preparedBuy);
|
||||
var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList();
|
||||
Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}");
|
||||
|
||||
// Execute a prepared Sell
|
||||
var sellResult = await bridge.Execute(myWallet, preparedSell);
|
||||
var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList();
|
||||
Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}");
|
||||
|
||||
// Execute a prepared Transfer
|
||||
var transferResult = await bridge.Execute(myWallet, preparedTransfer);
|
||||
var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList();
|
||||
Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}");
|
||||
```
|
||||
|
||||
## Onramping with Fiat
|
||||
The onramp flow will return a link for you to display/open as you please. You may poll the status of that onramp by its ID.
|
||||
In some cases, you may receive an additional set of onchain steps required to get to your destination token post on-ramp, in such cases, you may use our extension `IsSwapRequiredPostOnramp` to check, and if a swap is indeed required, you may use our `Execute` extensions to execute the transactions, or manually execute them by going through each `Step`.
|
||||
|
||||
```csharp
|
||||
// Onramp - Get a quote for buying crypto with Fiat
|
||||
var preparedOnramp = await bridge.Onramp_Prepare(
|
||||
onramp: OnrampProvider.Coinbase,
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
||||
amount: "10000000",
|
||||
receiver: await myWallet.GetAddress()
|
||||
);
|
||||
Console.WriteLine($"Onramp link: {preparedOnramp.Link}");
|
||||
Console.WriteLine($"Full onramp quote and steps data: {JsonConvert.SerializeObject(preparedOnramp, Formatting.Indented)}");
|
||||
|
||||
while (true)
|
||||
{
|
||||
var onrampStatus = await bridge.Onramp_Status(id: preparedOnramp.Id);
|
||||
Console.WriteLine($"Full Onramp Status: {JsonConvert.SerializeObject(onrampStatus, Formatting.Indented)}");
|
||||
if (onrampStatus.StatusType is StatusType.COMPLETED or StatusType.FAILED)
|
||||
{
|
||||
break;
|
||||
}
|
||||
await ThirdwebTask.Delay(5000);
|
||||
}
|
||||
|
||||
if (preparedOnramp.IsSwapRequiredPostOnramp())
|
||||
{
|
||||
// Execute additional steps that are required post-onramp to get to your token, manually or via the Execute extension
|
||||
var receipts = await bridge.Execute(myWallet, preparedOnramp);
|
||||
Console.WriteLine($"Onramp receipts: {JsonConvert.SerializeObject(receipts, Formatting.Indented)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No additional steps required post-onramp, you can use the tokens directly!");
|
||||
}
|
||||
```
|
||||
@@ -1,534 +1,310 @@
|
||||
import { Details, createMetadata } from "@doc";
|
||||
import { Callout, Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Common Utils | Thirdweb .NET SDK",
|
||||
description: "Useful utilities offered by thirdweb.",
|
||||
title: "Common Utils | Thirdweb .NET SDK",
|
||||
description: "Frequently used helpers for working with the Thirdweb .NET SDK.",
|
||||
});
|
||||
|
||||
# Utils
|
||||
|
||||
A collection of common utility functions that are useful for interacting with the Thirdweb .NET SDK or the blockchain in general. Full list available [here](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Utils.html).
|
||||
`Utils` bundles the SDK's most handy helpers for formatting values, working with addresses, and preparing typed data. If you need an API not covered here, check the [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Utils.html).
|
||||
|
||||
## Constants.ADDRESS_ZERO
|
||||
|
||||
A `string` representing the zero address `0x0000000000000000000000000000000000000000`.
|
||||
|
||||
Useful for passing the zero address to various functions.
|
||||
|
||||
## Constants.NATIVE_TOKEN_ADDRESS
|
||||
|
||||
A `string` representing the native token address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`.
|
||||
|
||||
Useful for passing the native token address (ETH or equivalent) to various functions.
|
||||
|
||||
## Constants.ENTRYPOINT_ADDRESS_V06
|
||||
|
||||
A `string` representing the entrypoint address for Account Abstraction 0.7.0 `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`.
|
||||
|
||||
## Constants.ENTRYPOINT_ADDRESS_V07
|
||||
|
||||
A `string` representing the entrypoint address for Account Abstraction 0.7.0 `0x0000000071727De22E5E9d8BAf0edAc6f37da032`.
|
||||
|
||||
## GetSocialProfiles
|
||||
|
||||
Fetches the social profiles for the given address or ENS using a Thirdweb client.
|
||||
## Useful constants
|
||||
|
||||
```csharp
|
||||
var socialProfiles = await Utils.GetSocialProfiles(client, addressOrEns);
|
||||
Constants.ADDRESS_ZERO // 0x0000…0000 (zero address)
|
||||
Constants.NATIVE_TOKEN_ADDRESS // 0xEeee…EEeE (native token sentinel)
|
||||
Constants.DECIMALS_18 // 1e18 as a double (decimal scaling helper)
|
||||
|
||||
Constants.IERC20_INTERFACE_ID // ERC-165 ID for ERC-20
|
||||
Constants.IERC721_INTERFACE_ID // ERC-165 ID for ERC-721
|
||||
Constants.IERC1155_INTERFACE_ID // ERC-165 ID for ERC-1155
|
||||
|
||||
Constants.ENTRYPOINT_ADDRESS_V06 // Default ERC-4337 entry point v0.6
|
||||
Constants.ENTRYPOINT_ADDRESS_V07 // Default ERC-4337 entry point v0.7
|
||||
|
||||
Constants.DEFAULT_FACTORY_ADDRESS_V06 // Default Thirdweb factory v0.6
|
||||
Constants.DEFAULT_FACTORY_ADDRESS_V07 // Default Thirdweb factory v0.7
|
||||
|
||||
Constants.EIP_1271_MAGIC_VALUE // Expected return value for EIP-1271 signature validation
|
||||
Constants.ERC_6492_MAGIC_VALUE // Magic value appended to ERC-6492 signatures
|
||||
```
|
||||
|
||||
## Chain helpers
|
||||
|
||||
### Utils.GetChainMetadata
|
||||
|
||||
Fetch metadata (native token, RPCs, explorers, stack type, etc.) for a given chain ID. Results are cached in-memory.
|
||||
|
||||
```csharp
|
||||
ThirdwebChainData data = await Utils.GetChainMetadata(client, chainId);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client
|
||||
|
||||
A `ThirdwebClient` object representing the client to use for fetching social profiles.
|
||||
|
||||
### addressOrEns
|
||||
|
||||
A `string` representing the wallet address or ENS to fetch social profiles for.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### socialProfiles
|
||||
|
||||
A `SocialProfiles` object containing the fetched social profiles.
|
||||
|
||||
</Details>
|
||||
|
||||
## GetENSFromAddress
|
||||
|
||||
Fetches the ENS name for the given address using a Thirdweb client.
|
||||
|
||||
```csharp
|
||||
var ensName = await Utils.GetENSFromAddress(client, address);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client
|
||||
|
||||
A `ThirdwebClient` object representing the client to use for fetching the ENS name.
|
||||
|
||||
### address
|
||||
|
||||
A `string` representing the wallet address to fetch the ENS name for.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### ensName
|
||||
|
||||
A `string` representing the fetched ENS name.
|
||||
|
||||
</Details>
|
||||
|
||||
## GetAddressFromENS
|
||||
|
||||
Fetches the address for the given ENS name using a Thirdweb client.
|
||||
|
||||
```csharp
|
||||
var address = await Utils.GetAddressFromENS(client, ensName);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client
|
||||
|
||||
A `ThirdwebClient` object representing the client to use for fetching the address.
|
||||
|
||||
### ensName
|
||||
|
||||
A `string` representing the ENS name to fetch the address for.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### address
|
||||
|
||||
A `string` representing the fetched address.
|
||||
|
||||
</Details>
|
||||
|
||||
## GetChainMetadata
|
||||
|
||||
Fetches chain data for a specified chain ID using a Thirdweb client.
|
||||
|
||||
```csharp
|
||||
var chainData = await Utils.GetChainMetadata (client, chainId);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client
|
||||
|
||||
A `ThirdwebClient` object representing the client to use for fetching chain data.
|
||||
An initialized `ThirdwebClient`.
|
||||
|
||||
### chainId
|
||||
|
||||
A `BigInteger` representing the chain ID to fetch data for.
|
||||
`BigInteger` chain identifier.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
<Details summary="Return value">
|
||||
|
||||
### chainData
|
||||
|
||||
A `ThirdwebChainData` object containing the fetched chain data such as native token name, symbol, decimals, etc.
|
||||
`ThirdwebChainData` describing the chain.
|
||||
|
||||
</Details>
|
||||
|
||||
## HexConcat
|
||||
### Utils.IsZkSync
|
||||
|
||||
Concatenates the given hex strings into a single hex string.
|
||||
Quickly check if a chain uses zkSync infrastructure. Falls back to chain metadata when the ID is not one of the known zkSync networks.
|
||||
|
||||
```csharp
|
||||
var concatenatedHex = Utils.HexConcat("0x1234", "0x5678");
|
||||
bool isZkSync = await Utils.IsZkSync(client, chainId);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### hexStrings
|
||||
### client
|
||||
|
||||
An array of hexadecimal `string` values to concatenate.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### concatenatedHex
|
||||
|
||||
The concatenated hexadecimal `string`.
|
||||
|
||||
</Details>
|
||||
|
||||
## HashPrefixedMessage
|
||||
|
||||
Hashes the given message bytes with a prefixed message for Ethereum signing.
|
||||
|
||||
```csharp
|
||||
var hashedMessage = messageBytes.HashPrefixedMessage();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### messageBytes
|
||||
|
||||
A `byte[]` representing the message to hash.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### hashedMessage
|
||||
|
||||
A `byte[]` representing the hashed message.
|
||||
|
||||
</Details>
|
||||
|
||||
## HashMessage
|
||||
|
||||
Hashes the given message using SHA3 Keccak hashing.
|
||||
|
||||
```csharp
|
||||
var hashedMessage = message.HashMessage();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### message
|
||||
|
||||
A `string` representing the message to hash.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### hashedMessage
|
||||
|
||||
A `string` representing the hashed message in hexadecimal format.
|
||||
|
||||
</Details>
|
||||
|
||||
## BytesToHex
|
||||
|
||||
Converts a byte array to a hexadecimal string.
|
||||
|
||||
```csharp
|
||||
var hexString = byteArray.BytesToHex();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### bytes
|
||||
|
||||
A `byte[]` representing the byte array to convert.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### hexString
|
||||
|
||||
A `string` representing the converted hexadecimal string.
|
||||
|
||||
</Details>
|
||||
|
||||
## HexToBytes
|
||||
|
||||
Converts a hexadecimal string to a byte array.
|
||||
|
||||
```csharp
|
||||
var byteArray = hexString.HexToBytes();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### hex
|
||||
|
||||
A `string` representing the hexadecimal string to convert.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### byteArray
|
||||
|
||||
A `byte[]` representing the converted byte array.
|
||||
|
||||
</Details>
|
||||
|
||||
## StringToHex
|
||||
|
||||
Converts a regular string to a hexadecimal string.
|
||||
|
||||
```csharp
|
||||
var hexString = "Hello".StringToHex();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### str
|
||||
|
||||
A `string` representing the regular string to convert.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### hexString
|
||||
|
||||
A `string` representing the converted hexadecimal string.
|
||||
|
||||
</Details>
|
||||
|
||||
## GetUnixTimeStampNow
|
||||
|
||||
Gets the current Unix timestamp in seconds.
|
||||
|
||||
```csharp
|
||||
var timestampNow = Utils.GetUnixTimeStampNow();
|
||||
```
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### timestampNow
|
||||
|
||||
A `long` representing the current Unix timestamp.
|
||||
|
||||
</Details>
|
||||
|
||||
## GetUnixTimeStampIn10Years
|
||||
|
||||
Gets the Unix timestamp for 10 years from now.
|
||||
|
||||
```csharp
|
||||
var timestampIn10Years = Utils.GetUnixTimeStampIn10Years();
|
||||
```
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### timestampIn10Years
|
||||
|
||||
A `long` representing the Unix timestamp 10 years from now.
|
||||
|
||||
</Details>
|
||||
|
||||
## ReplaceIPFS
|
||||
|
||||
Replaces an IPFS URI with a specified gateway.
|
||||
|
||||
```csharp
|
||||
var gatewayUri = ipfsUri.ReplaceIPFS("https://ipfs.io/ipfs/");
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### uri
|
||||
|
||||
A `string` representing the IPFS URI to replace.
|
||||
|
||||
### gateway
|
||||
|
||||
A `string` representing the gateway to use. Default is `Constants.FALLBACK_IPFS_GATEWAY`.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### gatewayUri
|
||||
|
||||
A `string` representing the replaced URI with the specified gateway.
|
||||
|
||||
</Details>
|
||||
|
||||
## ToWei
|
||||
|
||||
Converts Ether values to Wei.
|
||||
|
||||
```csharp
|
||||
var weiValue = ethValue.ToWei();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### eth
|
||||
|
||||
A `string` representing the Ether value to convert.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### weiValue
|
||||
|
||||
A `string` representing the converted Wei value.
|
||||
|
||||
</Details>
|
||||
|
||||
## ToEth
|
||||
|
||||
Converts Wei values to Ether.
|
||||
|
||||
```csharp
|
||||
var ethValue = weiValue.ToEth();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### wei
|
||||
|
||||
A `string` representing the Wei value to convert.
|
||||
|
||||
### decimalsToDisplay
|
||||
|
||||
An `int` specifying the number of decimals to display. Default is `4`.
|
||||
|
||||
### addCommas
|
||||
|
||||
A `bool` indicating whether to add commas to the output. Default is `false`.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### ethValue
|
||||
|
||||
A `string` representing the converted Ether value.
|
||||
|
||||
</Details>
|
||||
|
||||
## GenerateSIWE
|
||||
|
||||
Generates a Sign-In With Ethereum (SIWE) message.
|
||||
|
||||
```csharp
|
||||
var siweMessage = Utils.GenerateSIWE(loginPayloadData);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### loginPayloadData
|
||||
|
||||
A `LoginPayloadData` object containing the payload data for generating the SIWE message.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### siweMessage
|
||||
|
||||
A `string` representing the generated SIWE message.
|
||||
|
||||
</Details>
|
||||
|
||||
## IsZkSync
|
||||
|
||||
Checks if a given chain ID corresponds to zkSync.
|
||||
|
||||
```csharp
|
||||
var isZkSync = Utils.IsZkSync(chainId);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
`ThirdwebClient` used for metadata lookups.
|
||||
|
||||
### chainId
|
||||
|
||||
A `BigInteger` representing the chain ID to check.
|
||||
`BigInteger` chain identifier.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
<Details summary="Return value">
|
||||
|
||||
### isZkSync
|
||||
|
||||
A `bool` indicating whether the chain ID corresponds to zkSync.
|
||||
`true` if the chain is zkSync-based.
|
||||
|
||||
</Details>
|
||||
|
||||
## ToChecksumAddress
|
||||
## Address & identity helpers
|
||||
|
||||
Converts an Ethereum address to its checksum format.
|
||||
### Utils.ToChecksumAddress
|
||||
|
||||
Normalize any hex address to its EIP-55 checksum representation.
|
||||
|
||||
```csharp
|
||||
var checksummedAddress = address.ToChecksumAddress();
|
||||
string checksum = address.ToChecksumAddress();
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
### Utils.IsValidAddress
|
||||
|
||||
### address
|
||||
|
||||
A `string` representing the Ethereum address to convert.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### checksummedAddress
|
||||
|
||||
A `string` representing the checksummed Ethereum address.
|
||||
|
||||
</Details>
|
||||
|
||||
## AdjustDecimals
|
||||
|
||||
Adjusts the decimal places of a value.
|
||||
Lightweight validation for EVM addresses.
|
||||
|
||||
```csharp
|
||||
var adjustedValue = value.AdjustDecimals(18, 6);
|
||||
if (!input.IsValidAddress()) {
|
||||
throw new ArgumentException("Expected an EVM address");
|
||||
}
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
### Utils.GetENSFromAddress / Utils.GetAddressFromENS
|
||||
|
||||
### value
|
||||
|
||||
A `BigInteger` representing the value to adjust.
|
||||
|
||||
### fromDecimals
|
||||
|
||||
An `int` specifying the original number of decimals.
|
||||
|
||||
### toDecimals
|
||||
|
||||
An `int` specifying the target number of decimals.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### adjustedValue
|
||||
|
||||
A `BigInteger` representing the value adjusted to the new decimals.
|
||||
|
||||
</Details>
|
||||
|
||||
## ToJsonExternalWalletFriendly
|
||||
|
||||
Converts typed data into a JSON string suitable for external wallet interaction.
|
||||
Resolve between ENS names and addresses. Lookups are cached per process.
|
||||
|
||||
```csharp
|
||||
var jsonString = Utils.ToJsonExternalWalletFriendly(typedData, message);
|
||||
string ens = await Utils.GetENSFromAddress(client, address);
|
||||
string addr = await Utils.GetAddressFromENS(client, ensName);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
### Utils.GetSocialProfiles
|
||||
|
||||
### typedData
|
||||
Pull public social metadata (Lens, Farcaster, etc.) for an address or ENS.
|
||||
|
||||
A `TypedData<TDomain>` object representing the typed data to convert.
|
||||
```csharp
|
||||
SocialProfiles profiles = await Utils.GetSocialProfiles(client, "vitalik.eth");
|
||||
```
|
||||
|
||||
### message
|
||||
## Encoding & hashing
|
||||
|
||||
A `TMessage` object representing the message to include in the JSON.
|
||||
### Utils.HexConcat
|
||||
|
||||
</Details>
|
||||
Join multiple hex strings (with `0x` prefixes) into a single blob.
|
||||
|
||||
<Details summary="Return Value">
|
||||
```csharp
|
||||
string callData = Utils.HexConcat("0x1234", "0x5566");
|
||||
```
|
||||
|
||||
### jsonString
|
||||
### BytesToHex / HexToBytes
|
||||
|
||||
A `string` representing the JSON formatted data.
|
||||
Extension helpers for toggling between byte arrays and hex.
|
||||
|
||||
```csharp
|
||||
byte[] bytes = hex.HexToBytes();
|
||||
string hexAgain = bytes.BytesToHex();
|
||||
```
|
||||
|
||||
### HexToNumber / NumberToHex
|
||||
|
||||
Convert between hex strings and `BigInteger` values.
|
||||
|
||||
```csharp
|
||||
BigInteger gas = "0x5208".HexToNumber();
|
||||
string hexValue = gas.NumberToHex();
|
||||
```
|
||||
|
||||
### StringToHex / HexToString
|
||||
|
||||
Encode or decode UTF-8 strings as hex.
|
||||
|
||||
```csharp
|
||||
string payload = "Hello".StringToHex();
|
||||
string message = payload.HexToString();
|
||||
```
|
||||
|
||||
### HashPrefixedMessage & HashMessage
|
||||
|
||||
Generate Ethereum-style prefixed hashes or raw Keccak hashes.
|
||||
|
||||
```csharp
|
||||
byte[] prefixed = messageBytes.HashPrefixedMessage();
|
||||
string keccak = message.HashMessage();
|
||||
```
|
||||
|
||||
### Utils.HexToBytes32
|
||||
|
||||
Left-pad a hex string so it fits into exactly 32 bytes.
|
||||
|
||||
```csharp
|
||||
byte[] padded = someHex.HexToBytes32();
|
||||
```
|
||||
|
||||
### Utils.TrimZeroes
|
||||
|
||||
Strip leading zero bytes—handy before ABI encoding.
|
||||
|
||||
```csharp
|
||||
byte[] trimmed = signature.TrimZeroes();
|
||||
```
|
||||
|
||||
### Utils.PreprocessTypedDataJson
|
||||
|
||||
Stringify large integers inside a typed-data payload before sending it to browsers or wallets.
|
||||
|
||||
```csharp
|
||||
string normalizedJson = Utils.PreprocessTypedDataJson(rawJson);
|
||||
```
|
||||
|
||||
### Utils.ToJsonExternalWalletFriendly
|
||||
|
||||
Convert strongly typed EIP-712 data structures into wallet-ready JSON.
|
||||
|
||||
```csharp
|
||||
string json = Utils.ToJsonExternalWalletFriendly(typedData, message);
|
||||
```
|
||||
|
||||
### Utils.SerializeErc6492Signature
|
||||
|
||||
Wrap signatures for ERC-6492 (counterfactual ERC-4337 accounts) with the required magic value.
|
||||
|
||||
```csharp
|
||||
string wrapped = Utils.SerializeErc6492Signature(factoryAddress, deploymentCalldata, signatureBytes);
|
||||
```
|
||||
|
||||
## Value & time helpers
|
||||
|
||||
### Utils.ToWei / Utils.ToEth / Utils.FormatERC20
|
||||
|
||||
Convert between human-readable token amounts and Wei.
|
||||
|
||||
```csharp
|
||||
string wei = "0.05".ToWei();
|
||||
string eth = wei.ToEth(decimalsToDisplay: 6, addCommas: true);
|
||||
string usdc = Utils.FormatERC20(usdcRawValue, decimalsToDisplay: 2, decimals: 6);
|
||||
```
|
||||
|
||||
### Utils.AdjustDecimals
|
||||
|
||||
Scale a `BigInteger` between two decimal systems—useful when toggling between token decimals.
|
||||
|
||||
```csharp
|
||||
BigInteger stableValue = amount.AdjustDecimals(18, 6);
|
||||
```
|
||||
|
||||
### Utils.GenerateSIWE
|
||||
|
||||
Assemble a Sign-In With Ethereum message from a `LoginPayloadData` object. Throws if required fields are missing to help surface misconfigurations early.
|
||||
|
||||
```csharp
|
||||
string messageToSign = Utils.GenerateSIWE(payload);
|
||||
```
|
||||
|
||||
### Utils.GetUnixTimeStampNow / Utils.GetUnixTimeStampIn10Years
|
||||
|
||||
Timestamp helpers for authentication windows or scheduled expirations.
|
||||
|
||||
```csharp
|
||||
long now = Utils.GetUnixTimeStampNow();
|
||||
long farFuture = Utils.GetUnixTimeStampIn10Years();
|
||||
```
|
||||
|
||||
### Utils.ReplaceIPFS
|
||||
|
||||
Swap `ipfs://` URIs for a gateway of your choice (defaults to the Thirdweb fallback gateway).
|
||||
|
||||
```csharp
|
||||
string httpUrl = ipfsUri.ReplaceIPFS("https://w3s.link/ipfs/");
|
||||
```
|
||||
|
||||
## Gas & network costs
|
||||
|
||||
### Utils.FetchGasPrice / Utils.FetchGasFees
|
||||
|
||||
Retrieve legacy gas prices or EIP-1559 fee suggestions with optional safety bumps. Polygon, Celo, and Arbitrum chains get tailor-made logic.
|
||||
|
||||
```csharp
|
||||
BigInteger gasPrice = await Utils.FetchGasPrice(client, chainId);
|
||||
(BigInteger maxFee, BigInteger maxPriority) = await Utils.FetchGasFees(client, chainId);
|
||||
```
|
||||
|
||||
### Utils.IsEip155Enforced / Utils.IsEip1559Supported
|
||||
|
||||
Runtime checks that tell you whether a chain requires EIP-155 signatures and whether it supports EIP-1559 style fees.
|
||||
|
||||
```csharp
|
||||
bool needsProtectedTx = await Utils.IsEip155Enforced(client, chainId);
|
||||
bool supports1559 = Utils.IsEip1559Supported(chainId.ToString());
|
||||
```
|
||||
|
||||
## Transactions & monitoring
|
||||
|
||||
### Utils.WaitForTransactionReceipt
|
||||
|
||||
Poll for a transaction receipt with built-in timeouts from the client's fetch options.
|
||||
|
||||
```csharp
|
||||
var receipt = await Utils.WaitForTransactionReceipt(client, chainId, txHash);
|
||||
```
|
||||
|
||||
### Utils.IsDeployed
|
||||
|
||||
Check whether bytecode exists at an address.
|
||||
|
||||
```csharp
|
||||
bool deployed = await Utils.IsDeployed(client, chainId, contractAddress);
|
||||
```
|
||||
|
||||
### Utils.IsDelegatedAccount
|
||||
|
||||
Detect delegated accounts (EIP-7702 style) by comparing on-chain code against the delegation contract.
|
||||
|
||||
```csharp
|
||||
bool delegated = await Utils.IsDelegatedAccount(client, chainId, address, delegationContract);
|
||||
```
|
||||
|
||||
## IP & social enrichment
|
||||
|
||||
### Utils.PacketToBytes
|
||||
|
||||
Encode ENS-style DNS packets. Exposed for advanced ENS tooling; most integrations can rely on the higher-level ENS helpers above.
|
||||
|
||||
```csharp
|
||||
byte[] packet = Utils.PacketToBytes("subdomain.example.eth");
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "EcosystemWallet.Create | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate an EcosystemWallet for secure user authentication and interaction.",
|
||||
});
|
||||
|
||||
# EcosystemWallet.Create
|
||||
|
||||
Create an instance of `EcosystemWallet` using a user's email, phone number or OAuth. This wallet type facilitates secure user authentication through OTP verification, making it suitable for client-facing applications where handling private keys directly is not ideal.
|
||||
|
||||
## Login Methods
|
||||
|
||||
Ecosystem Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, TikTok etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a separate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// NOTE: All creation examples below may take in an `ecosystemPartnerId` if you are the ecosystem partner integrating with a third-party ecosystem
|
||||
|
||||
// Email
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", email: "userEmail");
|
||||
// Phone
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
|
||||
// Google, Apple, Facebook, etc.
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
// Custom Auth - JWT
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.JWT);
|
||||
// Custom Auth - AuthEndpoint
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.AuthEndpoint);
|
||||
// Guest Login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Guest);
|
||||
// Server Login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// Siwe
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// SiweExternal
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.SiweExternal);
|
||||
|
||||
// Session resuming supported for all methods
|
||||
var isConnected = await wallet.IsConnected();
|
||||
|
||||
// If not connected, initiate login flow based on the auth provider you are using
|
||||
|
||||
// Email & Phone (OTP)
|
||||
await wallet.SendOTP(); // and fetch the otp
|
||||
var address = await wallet.LoginWithOtp("userEnteredOTP"); // try catch and retry if needed
|
||||
|
||||
// Socials (OAuth)
|
||||
var address = await wallet.LoginWithOauth(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
mobileRedirectScheme: "myBundleId://"
|
||||
);
|
||||
|
||||
// Custom Auth (JWT)
|
||||
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
|
||||
|
||||
// Custom Auth (AuthEndpoint)
|
||||
var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
|
||||
|
||||
// Guest Login (Easy onboarding)
|
||||
var address = await wallet.LoginWithGuest();
|
||||
|
||||
// Backend (Server Wallets)
|
||||
var address = await wallet.LoginWithBackend();
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// SiweExternal (React-only wallet)
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
|
||||
An instance of `ThirdwebClient`.
|
||||
|
||||
### ecosystemId (required)
|
||||
|
||||
The ID of the ecosystem wallet you created on the dashboard.
|
||||
|
||||
### ecosystemPartnerId (optional)
|
||||
|
||||
The partner ID you were provided by the Ecosystem owner.
|
||||
|
||||
### email (optional)
|
||||
|
||||
The user's email address. Required if `phoneNumber` is not provided.
|
||||
|
||||
### phoneNumber (optional)
|
||||
|
||||
The user's phone number. Required if `email` is not provided.
|
||||
|
||||
### authProvider (optional)
|
||||
|
||||
The OAuth provider to use for authentication. Supported values are `AuthProvider.Google`, `AuthProvider.Apple`, `AuthProvider.Facebook`.
|
||||
|
||||
### storageDirectoryPath (optional)
|
||||
|
||||
The path to the directory where the wallet data is stored. Defaults to the application's data directory.
|
||||
|
||||
### siweSigner (optional)
|
||||
|
||||
An external wallet instance to use for SIWE authentication.
|
||||
|
||||
### legacyEncryptionKey (optional)
|
||||
|
||||
The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated.
|
||||
|
||||
### walletSecret (optional)
|
||||
|
||||
The secret identifier to use when creating server-side wallets with backend authentication.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### EcosystemWallet
|
||||
|
||||
Returns an instance of EcosystemWallet, initialized for the user based on the provided email or phone number. This wallet is ready for OTP authentication and further blockchain interactions.
|
||||
|
||||
</Details>
|
||||
|
||||
## OTP Authentication Flow
|
||||
|
||||
The OTP authentication flow involves sending an OTP to the user's email or phone and then verifying the OTP to complete authentication:
|
||||
|
||||
**Send OTP:** Initiate the login process by calling SendOTP on the EcosystemWallet instance. This sends an OTP to the user's email or phone number.
|
||||
|
||||
```csharp
|
||||
await wallet.SendOTP();
|
||||
```
|
||||
|
||||
**Submit OTP:** Once the user receives the OTP, they submit it back to the application, which then calls LoginWithOtp on the EcosystemWallet instance to verify the OTP and complete the login process.
|
||||
|
||||
```csharp
|
||||
var address = await wallet.LoginWithOtp("userEnteredOTP");
|
||||
// If this fails, feel free to catch and take in another OTP and retry the login process
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of creating an `EcosystemWallet` with a user's email and completing the OTP authentication flow:
|
||||
|
||||
```csharp
|
||||
// Create EcosystemWallet wallet as signer to unlock web2 auth
|
||||
var ecosystemWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
|
||||
|
||||
// Resume session (if `EcosystemWallet` wallet was not logged in)
|
||||
if (!await ecosystemWallet.IsConnected())
|
||||
{
|
||||
await ecosystemWallet.SendOTP();
|
||||
Console.WriteLine("Please submit the OTP.");
|
||||
var otp = Console.ReadLine();
|
||||
var ecosystemWalletAddress = await ecosystemWallet.LoginWithOtp(otp); // try catch and retry if needed
|
||||
}
|
||||
|
||||
Console.WriteLine($"EcosystemWallet address: {await ecosystemWallet.GetAddress()}");
|
||||
|
||||
// Sign a message
|
||||
var message = "Hello, Thirdweb!";
|
||||
var signature = await wallet.PersonalSign(message);
|
||||
Console.WriteLine($"Signature: {signature}");
|
||||
```
|
||||
|
||||
**Note:** EcosystemWallet leverages the security of OTP-based authentication to ensure a secure and user-friendly experience in blockchain applications.
|
||||
|
||||
## OAuth Authentication Flow
|
||||
|
||||
**LoginWithOauth:** Initiate the login process by calling LoginWithOauth on the EcosystemWallet instance. This redirects the user to the OAuth provider's login page.
|
||||
|
||||
```csharp
|
||||
// Windows console app example
|
||||
var address = await ecosystemWallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
);
|
||||
// Godot standalone example
|
||||
var address = await ThirdwebManager.Instance.EcosystemWallet.LoginWithOauth(
|
||||
isMobile: OS.GetName() == "Android" || OS.GetName() == "iOS",
|
||||
browserOpenAction: (url) => OS.ShellOpen(url),
|
||||
mobileRedirectScheme: "thirdweb://"
|
||||
);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### isMobile
|
||||
|
||||
A `bool` indicating whether the application is running on a mobile platform.
|
||||
|
||||
### browserOpenAction
|
||||
|
||||
An `Action<string>` that opens the OAuth provider's login page in a browser.
|
||||
|
||||
### mobileRedirectScheme
|
||||
|
||||
The redirect scheme to use for mobile platforms. Defaults to `"thirdweb://"`.
|
||||
|
||||
### browser
|
||||
|
||||
An instance of `IThirdwebBrowser` to use for the OAuth login process. Defaults to `null`.
|
||||
|
||||
### cancellationToken
|
||||
|
||||
A `CancellationToken` to cancel the operation. Defaults to `default`.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### string
|
||||
|
||||
The EcosystemWallet address as a hexadecimal `string`.
|
||||
|
||||
</Details>
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of creating an `EcosystemWallet` using OAuth.
|
||||
|
||||
```csharp
|
||||
// Create EcosystemWallet wallet as signer to unlock web2 auth
|
||||
var ecosystemWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
|
||||
// Resume session (if `EcosystemWallet` wallet was not logged in)
|
||||
if (!await ecosystemWallet.IsConnected())
|
||||
{
|
||||
try {
|
||||
var address = await ecosystemWallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
);
|
||||
Console.WriteLine($"OAuth login successful. EcosystemWallet address: {address}");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"OAuth login failed: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `LoginWithOauth` API allows for custom browser handling, making it suitable for various application types and platforms.
|
||||
|
||||
## External Wallet Auth (Siwe & SiweExternal)
|
||||
|
||||
**LoginWithSiwe:** Initiate the login process by calling LoginWithSiwe on the EcosystemWallet instance. This will prompt the external wallet to sign a message instantly.
|
||||
|
||||
```csharp
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
```
|
||||
|
||||
**LoginWithSiweExternal:** Initiate the login process by calling LoginWithSiweExternal on the EcosystemWallet instance. This will initiate a browser-based login flow for external wallets that only support web platforms.
|
||||
|
||||
```csharp
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
Note: The parameters are similar to the OAuth flow, with the addition of `forceWalletIds` to specify the wallet IDs to force the user to use. Using a single wallet id will skip the wallet selection screen and directly open the wallet.
|
||||
|
||||
## Unified Identity - Account Linking
|
||||
|
||||
EcosystemWallet supports linking multiple authentication methods to a single user account. This feature enables users to access their account using different authentication methods, such as email, phone, or OAuth, without creating separate accounts for each method.
|
||||
|
||||
### Linking Accounts
|
||||
|
||||
```csharp
|
||||
var ecosystemWalletMain = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
if (!await ecosystemWalletMain.IsConnected())
|
||||
{
|
||||
_ = await ecosystemWalletMain.LoginWithOauth(
|
||||
isMobile: false,
|
||||
(url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
"thirdweb://",
|
||||
new EcosystemWalletBrowser()
|
||||
);
|
||||
}
|
||||
Console.WriteLine($"Main EcosystemWallet address: {await ecosystemWalletMain.GetAddress()}");
|
||||
|
||||
// Prepare Telegram
|
||||
var socialWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Telegram);
|
||||
// Link Telegram
|
||||
_ = await ecosystemWalletMain.LinkAccount(walletToLink: socialWallet,);
|
||||
|
||||
// Prepare Phone
|
||||
var phoneWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
|
||||
_ = await phoneWallet.SendOTP();
|
||||
var otp = Console.ReadLine();
|
||||
// Link Phone
|
||||
_ = await ecosystemWalletMain.LinkAccount(walletToLink: phoneWallet, otp: otp);
|
||||
```
|
||||
|
||||
### Getting Linked Accounts
|
||||
|
||||
```csharp
|
||||
List<LinkedAccount> linkedAccounts = await ecosystemWalletMain.GetLinkedAccounts();
|
||||
```
|
||||
|
||||
### Unlinking Accounts
|
||||
|
||||
```csharp
|
||||
List<LinkedAccount> linkedAccounts = await ecosystemWallet.GetLinkedAccounts();
|
||||
List<LinkedAccount> linkedAccountsAfterUnlinking = await ecosystemWallet.UnlinkAccount(linkedAccounts[0]);
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "EngineWallet.Create | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate a EngineWallet to sign transactions and messages.",
|
||||
});
|
||||
|
||||
# EngineWallet.Create
|
||||
|
||||
A .NET integration of thirdweb Transactions.
|
||||
|
||||
Instantiate a `Engine` with a given private key. This wallet is capable of signing transactions and messages, interacting with smart contracts, and performing other blockchain operations. **This wallet type is intended for backend applications only,** due to the sensitive nature of private keys.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// EngineWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
|
||||
var engineWallet = EngineWallet.Create(
|
||||
client: client,
|
||||
engineUrl: Environment.GetEnvironmentVariable("ENGINE_URL"),
|
||||
authToken: Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN"),
|
||||
walletAddress: Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS"),
|
||||
timeoutSeconds: null, // no timeout
|
||||
additionalHeaders: null // can set things like x-account-address if using basic session keys
|
||||
);
|
||||
|
||||
// Simple self transfer
|
||||
var receipt = await engineWallet.Transfer(chainId: 11155111, toAddress: await engineWallet.GetAddress(), weiAmount: 0);
|
||||
Console.WriteLine($"Receipt: {receipt}");
|
||||
```
|
||||
|
||||
<Callout variant="warning" title="Important">
|
||||
This method involves using an access token, it is meant to be used in a secure backend environment. Never expose access tokens in client-side code or store them in an insecure manner. Ideally, use environment variables or secure vaults to manage access tokens in backend services.
|
||||
</Callout>
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
|
||||
An instance of `ThirdwebClient`.
|
||||
|
||||
### engineUrl (required)
|
||||
|
||||
The URL of the Thirdweb Engine instance, get it from your Engine dashboard.
|
||||
|
||||
### authToken (required)
|
||||
|
||||
The access token for the Engine instance, get it from your Engine dashboard.
|
||||
|
||||
### walletAddress (required)
|
||||
|
||||
The address of the backend wallet that will be used to sign transactions. You can create a backend wallet using the Engine dashboard.
|
||||
|
||||
### timeoutSeconds (optional)
|
||||
|
||||
Enforce a timeout for requests to the Engine instance, in seconds.
|
||||
|
||||
### additionalHeaders (optional)
|
||||
|
||||
Additional headers to include in requests to the Engine instance, such as `x-account-address` for Account Abstraction/Session Key flows.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### EngineWallet
|
||||
|
||||
Returns an instance of `EngineWallet`, ready to sign transactions and messages with the provided access token.
|
||||
|
||||
</Details>
|
||||
@@ -1,144 +0,0 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "InAppWallet.Create | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate an InAppWallet for secure user authentication and interaction.",
|
||||
});
|
||||
|
||||
# InAppWallet.Create
|
||||
|
||||
Create an instance of `InAppWallet` using a user's email, phone number or OAuth. This wallet type facilitates secure user authentication through OTP verification, making it suitable for client-facing applications where handling private keys directly is not ideal.
|
||||
|
||||
## Login Methods
|
||||
|
||||
In-App Wallets support a variety of login methods:
|
||||
- Email (OTP Login)
|
||||
- Phone (OTP Login)
|
||||
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, TikTok etc.)
|
||||
- Custom Auth (OIDC Compatible)
|
||||
- Custom Auth (Generic Auth Endpoint)
|
||||
- Guest (Onboard easily, link other accounts later)
|
||||
- Backend (Server Wallets)
|
||||
- Siwe (Login with a separate wallet supported by the SDK)
|
||||
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// Email
|
||||
var wallet = await InAppWallet.Create(client: client, email: "userEmail");
|
||||
// Phone
|
||||
var wallet = await InAppWallet.Create(client: client, phoneNumber: "+1234567890");
|
||||
// Google, Apple, Facebook, etc.
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Google);
|
||||
// Custom Auth - JWT
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.JWT);
|
||||
// Custom Auth - AuthEndpoint
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.AuthEndpoint);
|
||||
// Guest Login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Guest);
|
||||
// Server Login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// Siwe
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
// SiweExternal
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.SiweExternal);
|
||||
|
||||
// Session resuming supported for all methods
|
||||
var isConnected = await wallet.IsConnected();
|
||||
|
||||
// If not connected, initiate login flow based on the auth provider you are using
|
||||
|
||||
// Email & Phone (OTP)
|
||||
await wallet.SendOTP(); // and fetch the otp
|
||||
var address = await wallet.LoginWithOtp("userEnteredOTP"); // try catch and retry if needed
|
||||
|
||||
// Socials (OAuth)
|
||||
var address = await wallet.LoginWithOauth(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
mobileRedirectScheme: "myBundleId://"
|
||||
);
|
||||
|
||||
// Custom Auth (JWT)
|
||||
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
|
||||
|
||||
// Custom Auth (AuthEndpoint)
|
||||
var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
|
||||
|
||||
// Guest Login (Easy onboarding)
|
||||
var address = await wallet.LoginWithGuest();
|
||||
|
||||
// Backend (Server Wallets)
|
||||
var address = await wallet.LoginWithBackend();
|
||||
|
||||
// SIWE (Wallet)
|
||||
var address = await siweWallet.LoginWithSiwe(chainId: 1);
|
||||
|
||||
// SiweExternal (React-only wallet)
|
||||
var address = await wallet.LoginWithSiweExternal(
|
||||
// Windows console app example, adaptable to any runtime
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
|
||||
);
|
||||
```
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
|
||||
An instance of `ThirdwebClient`.
|
||||
|
||||
### email (optional)
|
||||
|
||||
The user's email address. Required if `phoneNumber` is not provided.
|
||||
|
||||
### phoneNumber (optional)
|
||||
|
||||
The user's phone number. Required if `email` is not provided.
|
||||
|
||||
### authProvider (optional)
|
||||
|
||||
The OAuth provider to use for authentication. Supported values are `AuthProvider.Google`, `AuthProvider.Apple`, `AuthProvider.Facebook`.
|
||||
|
||||
### storageDirectoryPath (optional)
|
||||
|
||||
The path to the directory where the wallet data is stored. Defaults to the application's data directory.
|
||||
|
||||
### siweSigner (optional)
|
||||
|
||||
An external wallet instance to use for SIWE authentication.
|
||||
|
||||
### legacyEncryptionKey (optional)
|
||||
|
||||
The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated.
|
||||
|
||||
### walletSecret (optional)
|
||||
|
||||
The secret identifier to use when creating server-side wallets with backend authentication.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
|
||||
### InAppWallet
|
||||
|
||||
Returns an instance of InAppWallet, initialized for the user based on the provided email or phone number. This wallet is ready for OTP authentication and further blockchain interactions.
|
||||
|
||||
</Details>
|
||||
|
||||
## Authentication, Linking and more.
|
||||
|
||||
`InAppWallet` extends `EcosystemWallet`, refer to the [EcosystemWallet](/dotnet/wallets/providers/ecosystem-wallet) documentation for information about authentication, linking multiple auth providers to the same wallet, and more. All functionality outside of creation and third-party integrations is the same as `EcosystemWallet`.
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "PrivateKeyWallet.Create | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Instantiate a PrivateKeyWallet to sign transactions and messages.",
|
||||
});
|
||||
|
||||
# PrivateKeyWallet.Create
|
||||
|
||||
Instantiate a `PrivateKeyWallet` with a given private key. This wallet is capable of signing transactions and messages, interacting with smart contracts, and performing other blockchain operations. **This wallet type is intended for backend applications only,** due to the sensitive nature of private keys.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
var wallet = await PrivateKeyWallet.Create(client, "yourPrivateKeyHex");
|
||||
```
|
||||
|
||||
<Callout variant="warning" title="Important">
|
||||
This method involves using a raw private key, which should be handled with the
|
||||
utmost care. Never expose private keys in client-side code or store them in an
|
||||
insecure manner. Ideally, use environment variables or secure vaults to manage
|
||||
private keys in backend services.
|
||||
</Callout>
|
||||
|
||||
<Details summary="Parameters">
|
||||
|
||||
### client (required)
|
||||
|
||||
An instance of `ThirdwebClient`.
|
||||
|
||||
### privateKeyHex (required)
|
||||
|
||||
The private key of the wallet, provided as a hexadecimal `string`. This key grants full control over the wallet's funds and assets on the blockchain.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return Value">
|
||||
### PrivateKeyWallet
|
||||
|
||||
Returns an instance of `PrivateKeyWallet`, ready to sign transactions and messages with the provided private key.
|
||||
|
||||
</Details>
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example of creating a PrivateKeyWallet to sign a simple message. This example assumes you have a ThirdwebClient instance already created:
|
||||
|
||||
```csharp
|
||||
var client = ThirdwebClient.Create("yourClientId");
|
||||
var privateKeyHex = "yourPrivateKeyHex"; // Should be securely stored and accessed
|
||||
var wallet = await PrivateKeyWallet.Create(client, privateKeyHex);
|
||||
|
||||
Console.WriteLine($"PrivateKeyWallet address: {await wallet.GetAddress()}");
|
||||
|
||||
// Sign a message
|
||||
var message = "Hello, Thirdweb!";
|
||||
var signature = await wallet.PersonalSign(message);
|
||||
Console.WriteLine($"Signature: {signature}");
|
||||
```
|
||||
|
||||
**Note**: The `PrivateKeyWallet` is intended for use in secure, backend environments. Ensure private keys are never exposed or hard-coded in client-side applications. Use `InAppWallet` and `SmartWallet` for client-side applications.
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Callout, Details, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ServerWallet.Create | Thirdweb .NET SDK",
|
||||
description: "Provision a vault-backed server wallet from your Thirdweb dashboard label.",
|
||||
});
|
||||
|
||||
# ServerWallet.Create
|
||||
|
||||
`ServerWallet` lets you operate a vault-secured signer that lives inside your Thirdweb project. Use it whenever you need backend signing with the same controls and monitoring offered in the dashboard.
|
||||
|
||||
<Callout type="info">
|
||||
Before you instantiate a server wallet from code, create it inside the dashboard (Transactions → Server wallets) and note the label you assign. The SDK searches for that label when you call `Create`.
|
||||
</Callout>
|
||||
|
||||
## Quick start
|
||||
|
||||
```csharp
|
||||
var client = ThirdwebClient.Create(secretKey: Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"));
|
||||
|
||||
var wallet = await ServerWallet.Create(
|
||||
client: client,
|
||||
label: "production-deployer"
|
||||
);
|
||||
|
||||
Console.WriteLine($"Server wallet ready at {await wallet.GetAddress()}.");
|
||||
```
|
||||
|
||||
This call:
|
||||
|
||||
1. Reuses your client credentials to talk to the Thirdweb Engine API.
|
||||
2. Finds the server wallet whose label matches `production-deployer`.
|
||||
3. Auto-configures execution options that track the signer address and idempotency key.
|
||||
|
||||
## Execution options
|
||||
|
||||
The optional `executionOptions` argument lets you control how the wallet submits transactions. Pass one of the SDK-provided option types to enable advanced flows:
|
||||
|
||||
```csharp
|
||||
// Auto (default) – SDK picks the right strategy per chain
|
||||
var autoOptions = new AutoExecutionOptions
|
||||
{
|
||||
IdempotencyKey = Guid.NewGuid().ToString(),
|
||||
};
|
||||
|
||||
// EOA – force a direct signer submission
|
||||
var eoaOptions = new EOAExecutionOptions();
|
||||
|
||||
// ERC-4337 – sign and send as a smart account
|
||||
var erc4337Options = new ERC4337ExecutionOptions(
|
||||
chainId: new BigInteger(8453),
|
||||
signerAddress: "0xYourSigner"
|
||||
)
|
||||
{
|
||||
EntrypointAddress = "0xEntryPoint",
|
||||
};
|
||||
|
||||
// EIP-7702 – delegate to an authorized contract
|
||||
var eip7702Options = new EIP7702ExecutionOptions();
|
||||
|
||||
var advancedWallet = await ServerWallet.Create(
|
||||
client: client,
|
||||
label: "production-deployer",
|
||||
executionOptions: erc4337Options
|
||||
);
|
||||
```
|
||||
|
||||
`ServerWallet.Create` will populate any missing `From`, `SignerAddress`, or smart-account data at runtime. If you pass an unsupported execution option, the call throws an `InvalidOperationException` with guidance on the allowed types.
|
||||
|
||||
## Self-managed vault access
|
||||
|
||||
If you configured the wallet as self-managed, include the Vault Access Token issued by Thirdweb so your backend can sign requests on behalf of the vault.
|
||||
|
||||
```csharp
|
||||
var wallet = await ServerWallet.Create(
|
||||
client: client,
|
||||
label: "self-managed",
|
||||
vaultAccessToken: Environment.GetEnvironmentVariable("THIRDWEB_VAULT_ACCESS_TOKEN")
|
||||
);
|
||||
```
|
||||
|
||||
The method injects the token as an `X-Vault-Access-Token` header for all Engine API calls.
|
||||
|
||||
## Handling lookup failures
|
||||
|
||||
Creation will throw descriptive exceptions when it cannot find the target wallet:
|
||||
|
||||
- `ArgumentNullException` if you omit the `client` or `label`.
|
||||
- `InvalidOperationException` when no server wallets exist or the label does not match (the error lists available labels).
|
||||
|
||||
Wrap your call in a retry or surface the labels to operators to resolve quickly.
|
||||
|
||||
## Next steps
|
||||
|
||||
A server wallet implements `IThirdwebWallet`, so you can reuse it anywhere you expect a wallet, including:
|
||||
|
||||
- Contract writes via `await contract.Write(wallet, ...)`
|
||||
- Prepared transactions with `await contract.Prepare(wallet, ...)`
|
||||
- Direct Engine interactions through `wallet.SendTransaction`
|
||||
|
||||
For end-user wallets, see the [User Wallets](/dotnet/user-wallets) guide.
|
||||
|
||||
## API reference
|
||||
|
||||
<Details summary="ServerWallet.Create">
|
||||
|
||||
### Required
|
||||
|
||||
- `client`: `ThirdwebClient` — Initialized with your project secret key.
|
||||
- `label`: `string` — Matches the label from the dashboard.
|
||||
|
||||
### Optional
|
||||
|
||||
- `executionOptions`: `ExecutionOptions` — One of `AutoExecutionOptions`, `EOAExecutionOptions`, `EIP7702ExecutionOptions`, or `ERC4337ExecutionOptions`.
|
||||
- `vaultAccessToken`: `string` — Required when the vault is self-managed.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return value">
|
||||
|
||||
Returns an initialized `ServerWallet` instance tracked by the SDK connection manager. Use its methods to read the address, sign messages, or send transactions from your backend.
|
||||
|
||||
</Details>
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "User Wallets | Thirdweb .NET SDK",
|
||||
description:
|
||||
"Learn how to work with InAppWallet and EcosystemWallet using the unified user wallet APIs.",
|
||||
});
|
||||
|
||||
# User Wallets
|
||||
|
||||
`InAppWallet` and `EcosystemWallet` provide the same runtime capabilities for user authentication and signing in the Thirdweb .NET SDK. The only material differences are in how you create each wallet. Use this guide anytime you need an OTP-backed wallet experience for end users, regardless of whether you are building on the default thirdweb ecosystem or operating your own partner ecosystem.
|
||||
|
||||
## Choosing a wallet type
|
||||
|
||||
- **InAppWallet** — best for applications that rely on the default thirdweb infrastructure. Creation does not require an `ecosystemId`.
|
||||
- **EcosystemWallet** — required when you run your own ecosystem. Creation requires the `ecosystemId` that was issued in the dashboard (and optionally an `ecosystemPartnerId`).
|
||||
|
||||
Once created, both wallets expose identical methods for login, session management, and blockchain interactions.
|
||||
|
||||
## Creating wallets
|
||||
|
||||
### InAppWallet
|
||||
|
||||
```csharp
|
||||
// Email
|
||||
var wallet = await InAppWallet.Create(client: client, email: "userEmail");
|
||||
// Phone
|
||||
var wallet = await InAppWallet.Create(client: client, phoneNumber: "+1234567890");
|
||||
// Social login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Google);
|
||||
// Custom auth (JWT)
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.JWT);
|
||||
// Guest login
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Guest);
|
||||
// Backend (server wallet)
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// SIWE
|
||||
var wallet = await InAppWallet.Create(client: client, authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
```
|
||||
|
||||
### EcosystemWallet
|
||||
|
||||
```csharp
|
||||
// NOTE: Add ecosystemPartnerId if you are an approved partner within the ecosystem
|
||||
|
||||
// Email
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", email: "userEmail");
|
||||
// Phone
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
|
||||
// Social login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
// Custom auth (JWT)
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.JWT);
|
||||
// Custom auth (auth endpoint)
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.AuthEndpoint);
|
||||
// Guest login
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Guest);
|
||||
// Backend (server wallet)
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Backend, walletSecret: "very-secret");
|
||||
// SIWE
|
||||
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
|
||||
```
|
||||
|
||||
Once a wallet is created, you can always resume the session:
|
||||
|
||||
```csharp
|
||||
var isConnected = await wallet.IsConnected();
|
||||
```
|
||||
|
||||
If the wallet is not connected, initiate the login flow that corresponds to the auth provider you selected.
|
||||
|
||||
## Supported login methods
|
||||
|
||||
Both wallet types support the same login surfaces:
|
||||
|
||||
- Email or phone (OTP-based)
|
||||
- Social providers (Google, Apple, Facebook, Telegram, Farcaster, Line, GitHub, Twitch, Steam, TikTok, and more)
|
||||
- Custom auth (JWT or generic auth endpoint)
|
||||
- Guest onboarding
|
||||
- Backend/server wallets
|
||||
- Sign-In With Ethereum (SIWE)
|
||||
- SIWE External (web-only wallets via a browser prompt)
|
||||
|
||||
## OTP authentication flow
|
||||
|
||||
```csharp
|
||||
await wallet.SendOTP();
|
||||
var address = await wallet.LoginWithOtp("userEnteredOTP"); // retry on failure if needed
|
||||
```
|
||||
|
||||
Catch exceptions to allow retries when the user enters an incorrect code.
|
||||
|
||||
## OAuth flow
|
||||
|
||||
```csharp
|
||||
var address = await wallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
browserOpenAction: (url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
mobileRedirectScheme: "myBundleId://"
|
||||
);
|
||||
```
|
||||
|
||||
Adjust `isMobile`, `browserOpenAction`, and `mobileRedirectScheme` based on your target runtime. For Godot or other engines, reuse the existing platform-specific handlers you already have in place.
|
||||
|
||||
## Custom auth examples
|
||||
|
||||
```csharp
|
||||
// Custom Auth (JWT)
|
||||
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
|
||||
|
||||
// Custom Auth (AuthEndpoint)
|
||||
var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
|
||||
```
|
||||
|
||||
## Guest and backend flows
|
||||
|
||||
```csharp
|
||||
// Guest
|
||||
var guestAddress = await wallet.LoginWithGuest();
|
||||
|
||||
// Backend (server wallet)
|
||||
var backendAddress = await wallet.LoginWithBackend();
|
||||
```
|
||||
|
||||
## External wallet authentication
|
||||
|
||||
```csharp
|
||||
// SIWE
|
||||
var address = await wallet.LoginWithSiwe(chainId: 1);
|
||||
```
|
||||
|
||||
Limiting `forceWalletIds` to a single value skips the wallet picker and deep-links into that specific wallet provider.
|
||||
|
||||
## Unified identity and account linking
|
||||
|
||||
Link multiple login methods to the same user identity regardless of the wallet type that initiated the session.
|
||||
|
||||
```csharp
|
||||
var primaryWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
|
||||
if (!await primaryWallet.IsConnected())
|
||||
{
|
||||
_ = await primaryWallet.LoginWithOauth(
|
||||
isMobile: false,
|
||||
(url) =>
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
|
||||
_ = Process.Start(psi);
|
||||
},
|
||||
"thirdweb://"
|
||||
);
|
||||
}
|
||||
|
||||
var telegramWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Telegram);
|
||||
_ = await primaryWallet.LinkAccount(walletToLink: telegramWallet);
|
||||
|
||||
var phoneWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
|
||||
_ = await phoneWallet.SendOTP();
|
||||
var otp = Console.ReadLine();
|
||||
_ = await primaryWallet.LinkAccount(walletToLink: phoneWallet, otp: otp);
|
||||
|
||||
List<LinkedAccount> linkedAccounts = await primaryWallet.GetLinkedAccounts();
|
||||
List<LinkedAccount> linkedAccountsAfterUnlinking = await primaryWallet.UnlinkAccount(linkedAccounts[0]);
|
||||
```
|
||||
|
||||
## Parameter reference
|
||||
|
||||
<Details summary="InAppWallet.Create">
|
||||
|
||||
### Required
|
||||
|
||||
- `client`: `ThirdwebClient`
|
||||
|
||||
### Optional
|
||||
|
||||
- `email`: string — required if `phoneNumber` is not provided.
|
||||
- `phoneNumber`: string — required if `email` is not provided.
|
||||
- `authProvider`: `AuthProvider`
|
||||
- `storageDirectoryPath`: string
|
||||
- `siweSigner`: external wallet instance for SIWE.
|
||||
- `walletSecret`: string — required for backend/server wallet creation.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="EcosystemWallet.Create">
|
||||
|
||||
### Required
|
||||
|
||||
- `client`: `ThirdwebClient`
|
||||
- `ecosystemId`: string — your ecosystem identifier from the dashboard.
|
||||
|
||||
### Optional
|
||||
|
||||
- `ecosystemPartnerId`: string — partner identifier if supplied by the ecosystem owner.
|
||||
- `email`: string — required if `phoneNumber` is not provided.
|
||||
- `phoneNumber`: string — required if `email` is not provided.
|
||||
- `authProvider`: `AuthProvider`
|
||||
- `storageDirectoryPath`: string
|
||||
- `siweSigner`: external wallet instance for SIWE.
|
||||
- `walletSecret`: string — required for backend/server wallet creation.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Return value">
|
||||
|
||||
Both `Create` methods return an initialized instance of either `InAppWallet` or `EcosystemWallet`. These instances are ready for OTP verification, OAuth, or any other supported login flow and expose the same wallet interaction APIs thereafter.
|
||||
|
||||
</Details>
|
||||
@@ -1,3 +1,7 @@
|
||||
html {
|
||||
font-feature-settings: "liga" 1;
|
||||
}
|
||||
|
||||
code span {
|
||||
color: var(--code-light-color);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@workspace/ui/global.css";
|
||||
import "./globals.css";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { createMetadata } from "@/components/Document";
|
||||
@@ -9,18 +10,6 @@ import { EnableSmoothScroll } from "../components/others/SmoothScroll";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Header } from "./Header";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
const monoFont = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: "thirdweb developer portal",
|
||||
title: "thirdweb docs",
|
||||
@@ -34,7 +23,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(sansFont.variable, monoFont.variable, "font-sans")}
|
||||
className={cn(GeistMono.variable, GeistSans.variable, "font-sans")}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ThemeProvider
|
||||
|
||||
@@ -61,14 +61,33 @@ export const sidebar: SideBar = {
|
||||
href: `${paymentsSlug}/custom-data`,
|
||||
name: "Custom Data",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402`,
|
||||
name: "x402",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${paymentsSlug}/x402`,
|
||||
name: "Get Started",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/client`,
|
||||
name: "Client Side",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/server`,
|
||||
name: "Server Side",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/facilitator`,
|
||||
name: "Facilitator API",
|
||||
},
|
||||
],
|
||||
name: "x402",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint } from "@doc";
|
||||
import { TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
# Client Side
|
||||
|
||||
Make requests to any x402-compatible backend by automatically handling payment flows when APIs return a `402 Payment Required` response.
|
||||
|
||||
The client library wraps the native `fetch` API and handles:
|
||||
1. Initial request to the API
|
||||
2. Detection of `402 Payment Required` responses
|
||||
3. Parsing payment requirements from the response
|
||||
4. Creating and signing payment authorization
|
||||
5. Retrying the request with payment credentials
|
||||
|
||||
<Tabs defaultValue="typescript">
|
||||
<TabsList>
|
||||
<TabsTrigger value="typescript" className="flex items-center [&>p]:mb-0">
|
||||
<TypeScriptIcon className="size-4 mr-1.5" />
|
||||
TypeScript
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="http" className="flex items-center [&>p]:mb-0">
|
||||
<EngineIcon className="size-4 mr-1.5" />
|
||||
HTTP API
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
## Using `wrapFetchWithPayment`
|
||||
|
||||
The `wrapFetchWithPayment` function wraps the native fetch API to automatically handle 402 Payment Required responses.
|
||||
|
||||
```typescript
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
const wallet = createWallet("io.metamask"); // or any other wallet
|
||||
await wallet.connect({ client })
|
||||
|
||||
// Wrap fetch with payment handling
|
||||
// maxValue is the maximum payment amount in base units (defaults to 1 USDC = 1_000_000)
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
client,
|
||||
wallet,
|
||||
BigInt(1 * 10 ** 6) // max 1 USDC
|
||||
);
|
||||
|
||||
// Make a request that may require payment
|
||||
const response = await fetchWithPay('https://api.example.com/paid-endpoint');
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `fetch` - The fetch function to wrap (typically `globalThis.fetch`)
|
||||
- `client` - The thirdweb client used to access RPC infrastructure
|
||||
- `wallet` - The wallet used to sign payment messages
|
||||
- `maxValue` - (Optional) The maximum allowed payment amount in base units (defaults to 1 USDC = 1,000,000)
|
||||
|
||||
### Reference
|
||||
|
||||
For full API documentation, see the [TypeScript Reference](/references/typescript/v5/x402/wrapFetchWithPayment).
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="http">
|
||||
## Prepare Payment
|
||||
|
||||
Create a signed payment header from the authenticated user to include in your API request:
|
||||
|
||||
### Making the Paid Request
|
||||
|
||||
After preparing the payment, include it in your API request:
|
||||
|
||||
```bash
|
||||
# First, make the initial request to get payment requirements
|
||||
curl -X GET https://api.example.com/paid-endpoint
|
||||
|
||||
# Response will be 402 Payment Required with payment details
|
||||
# {
|
||||
# "x402Version": 1,
|
||||
# "accepts": [...payment requirements...],
|
||||
# "error": "Payment required"
|
||||
# }
|
||||
|
||||
# Select one of the payment methods, and sign the payment authorization using the API
|
||||
curl -X POST https://api.thirdweb.com/v1/payments/x402/prepare \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <user-access-token>" \
|
||||
-d '{
|
||||
"from": "0x1234...",
|
||||
"paymentRequirements": { ... }
|
||||
}'
|
||||
|
||||
# Response will contain the signed payment header
|
||||
# {
|
||||
# "paymentPayload": { ... },
|
||||
# "paymentHeader": "..." // base64 encoded payment header
|
||||
# }
|
||||
|
||||
# Finally, make the request with the payment header
|
||||
curl -X GET https://api.example.com/paid-endpoint \
|
||||
-H "X-PAYMENT: <base64-encoded-payment>"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
<OpenApiEndpoint path="/v1/payments/x402/prepare" method="POST" />
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,344 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout } from "@doc";
|
||||
import { TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
# Facilitator API
|
||||
|
||||
The facilitator is a service that handles verifying and submitting x402 payments. It uses your own [server wallet](/wallets/server) and leverages EIP-7702 to submit transactions gaslessly.
|
||||
|
||||
The thirdweb facilitator is compatible with any x402 backend and middleware libraries like `x402-hono`, `x402-next`, and more.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Verification** - Validates payment signatures and requirements
|
||||
2. **Settlement** - Submits the payment transaction on-chain
|
||||
3. **Gasless** - Uses [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) for gasless transactions
|
||||
4. **Your Wallet** - Uses your own server wallet for receiving payments
|
||||
|
||||
You can view all transactions processed by your facilitator in your project dashboard.
|
||||
|
||||
<Tabs defaultValue="typescript">
|
||||
<TabsList>
|
||||
<TabsTrigger value="typescript" className="flex items-center [&>p]:mb-0">
|
||||
<TypeScriptIcon className="size-4 mr-1.5" />
|
||||
TypeScript API
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="http" className="flex items-center [&>p]:mb-0">
|
||||
<EngineIcon className="size-4 mr-1.5" />
|
||||
HTTP API
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
## Creating a Facilitator
|
||||
|
||||
Create a facilitator instance to use with `settlePayment()` or external middleware:
|
||||
|
||||
```typescript
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: "your-secret-key",
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client: client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```typescript
|
||||
const thirdwebFacilitator = facilitator({
|
||||
// Required: Your thirdweb client with secret key
|
||||
client: client,
|
||||
|
||||
// Required: Your server wallet address that will execute transactions
|
||||
// get it from your project dashboard
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
|
||||
// Optional: Wait behavior for settlements
|
||||
// - "simulated": Only simulate the transaction (fastest)
|
||||
// - "submitted": Wait until transaction is submitted
|
||||
// - "confirmed": Wait for full on-chain confirmation (slowest, default)
|
||||
waitUntil: "confirmed",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage with settlePayment()
|
||||
|
||||
Use the facilitator with the `settlePayment()` function:
|
||||
|
||||
```typescript
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl: "https://api.example.com/premium-content",
|
||||
method: "GET",
|
||||
paymentData,
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.10",
|
||||
facilitator: thirdwebFacilitator, // Pass the facilitator here
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
return Response.json({ data: "premium content" });
|
||||
} else {
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with other x402 middleware libraries
|
||||
|
||||
Use the facilitator with third-party x402 middleware libraries:
|
||||
|
||||
<Tabs defaultValue="hono">
|
||||
<TabsList>
|
||||
<TabsTrigger value="hono">x402-hono</TabsTrigger>
|
||||
<TabsTrigger value="next">x402-next</TabsTrigger>
|
||||
<TabsTrigger value="express">x402-express</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="hono">
|
||||
```typescript
|
||||
import { Hono } from "hono";
|
||||
import { paymentMiddleware } from "x402-hono";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: "your-secret-key",
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client: client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Add the facilitator to the x402 middleware
|
||||
app.use(paymentMiddleware(
|
||||
"0xYourWalletAddress",
|
||||
{
|
||||
"/api/paywall": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
thirdwebFacilitator, // Pass the facilitator to the middleware
|
||||
)
|
||||
);
|
||||
|
||||
app.get("/api/paywall", (c) => {
|
||||
return c.json({ message: "This is premium content!" });
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="next">
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: process.env.SERVER_WALLET_ADDRESS,
|
||||
});
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
'0xYourWalletAddress',
|
||||
{
|
||||
"/api/premium": {
|
||||
price: "$0.05",
|
||||
network: "base-sepolia",
|
||||
},
|
||||
},
|
||||
thirdwebFacilitator // Pass the facilitator to the middleware
|
||||
);
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/premium/:path*"],
|
||||
};
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="express">
|
||||
```typescript
|
||||
import express from "express";
|
||||
import { paymentMiddleware } from "x402-express";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: "your-secret-key",
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client: client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
// Add the facilitator to the x402 middleware
|
||||
app.use(
|
||||
paymentMiddleware(
|
||||
"0xYourWalletAddress",
|
||||
{
|
||||
"/api/paywall": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
thirdwebFacilitator, // Pass the facilitator to the middleware
|
||||
)
|
||||
);
|
||||
|
||||
app.get("/api/paywall", (req, res) => {
|
||||
res.json({ message: "This content is behind a paywall!" });
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Getting Supported Payment Methods
|
||||
|
||||
Query which payment methods are supported by the facilitator:
|
||||
|
||||
```typescript
|
||||
// Get all supported payment methods
|
||||
const allSupported = await thirdwebFacilitator.supported();
|
||||
|
||||
// Filter by chain
|
||||
const baseSupported = await thirdwebFacilitator.supported({
|
||||
chainId: 8453, // Base
|
||||
});
|
||||
|
||||
// Filter by chain and token
|
||||
const usdcOnBase = await thirdwebFacilitator.supported({
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
});
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="http">
|
||||
## HTTP API Reference
|
||||
|
||||
Use the HTTP API directly if you're not using the TypeScript SDK.
|
||||
|
||||
### Example Flow
|
||||
|
||||
```bash
|
||||
# 1. First request returns 402 Payment Required
|
||||
curl -X GET https://api.example.com/premium \
|
||||
-H "Content-Type: application/json"
|
||||
|
||||
# Response (402):
|
||||
# {
|
||||
# "x402Version": 1,
|
||||
# "accepts": [{
|
||||
# "scheme": "exact",
|
||||
# "network": "eip155:421614",
|
||||
# "maxAmountRequired": "100000",
|
||||
# "resource": "https://api.example.com/premium",
|
||||
# "description": "Premium content access",
|
||||
# "mimeType": "application/json",
|
||||
# "payTo": "0x1234...",
|
||||
# "maxTimeoutSeconds": 300,
|
||||
# "asset": "0x..."
|
||||
# }],
|
||||
# "error": "Payment required"
|
||||
# }
|
||||
|
||||
# 2. Prepare payment (see Client Side docs)
|
||||
# This selects the payment method and creates a signed payment payload
|
||||
|
||||
# 3. Verify the payment (optional)
|
||||
curl -X POST https://api.thirdweb.com/v1/payments/x402/verify \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY" \
|
||||
-d '{
|
||||
"x402Version": 1,
|
||||
"paymentPayload": { ... },
|
||||
"paymentRequirements": { ... }
|
||||
}'
|
||||
|
||||
# 4. Settle the payment
|
||||
curl -X POST https://api.thirdweb.com/v1/payments/x402/settle \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY" \
|
||||
-d '{
|
||||
"x402Version": 1,
|
||||
"paymentPayload": { ... },
|
||||
"paymentRequirements": { ...},
|
||||
"waitUntil": "confirmed"
|
||||
}'
|
||||
```
|
||||
|
||||
<Callout variant="info">
|
||||
All facilitator HTTP API endpoints require authentication using the `x-secret-key` header with your project secret key.
|
||||
</Callout>
|
||||
|
||||
### Verify Payment
|
||||
|
||||
Verify a payment signature and requirements without settling:
|
||||
|
||||
<OpenApiEndpoint path="/v1/payments/x402/verify" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
### Settle Payment
|
||||
|
||||
Verify and submit a payment transaction on-chain:
|
||||
|
||||
<OpenApiEndpoint path="/v1/payments/x402/settle" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
### List Supported Payment Methods
|
||||
|
||||
Get a list of payment methods supported by the facilitator:
|
||||
|
||||
<OpenApiEndpoint path="/v1/payments/x402/supported" method="GET" />
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -1,28 +1,22 @@
|
||||
import { ArticleIconCard } from "@doc";
|
||||
import { ReactIcon } from "@/icons";
|
||||
import { ReactIcon, TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
# x402 payments
|
||||
# x402 Payments
|
||||
|
||||
Implement paid API calls using the x402 protocol. Every request is paid for by the user with a micro payment onchain.
|
||||
x402 is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
|
||||
|
||||
Learn more about the protocol at [x402.org](https://x402.org).
|
||||
|
||||
<ArticleIconCard
|
||||
title="x402 Playground"
|
||||
description="Try out a x402 payment in our live playground"
|
||||
description="Try out x402 payments in our live playground"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/payments/x402"
|
||||
/>
|
||||
|
||||
## Client Side
|
||||
|
||||
`wrapFetchWithPayment` wraps the native fetch API to automatically handle `402 Payment Required` responses from any API call. It will:
|
||||
1. Make the initial request
|
||||
2. If a 402 response is received, parse the payment requirements
|
||||
3. Verify the payment amount is within the allowed maximum
|
||||
4. Sign a payment authorization
|
||||
5. Create a payment header using the provided wallet signature
|
||||
6. Retry the request with the payment header
|
||||
|
||||
Here's an example:
|
||||
`wrapFetchWithPayment` wraps the native fetch API to automatically handle `402 Payment Required` responses from any x402-compatible API:
|
||||
|
||||
```typescript
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
@@ -30,7 +24,7 @@ import { createThirdwebClient } from "thirdweb";
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
const wallet = createWallet("io.metamask"); // or any other wallet
|
||||
const wallet = createWallet("io.metamask");
|
||||
await wallet.connect({ client })
|
||||
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
@@ -41,15 +35,12 @@ const response = await fetchWithPay('https://api.example.com/paid-endpoint');
|
||||
|
||||
## Server Side
|
||||
|
||||
To make your API calls payable, you can use the `settlePayment` function in a middleware or in your endpoint directly.
|
||||
|
||||
Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `settlePayment` function.
|
||||
|
||||
Here's an example with a Next.js middleware:
|
||||
To make your API calls payable, use the `settlePayment` function in a middleware or endpoint:
|
||||
|
||||
```typescript
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
const thirdwebX402Facilitator = facilitator({
|
||||
@@ -57,45 +48,54 @@ const thirdwebX402Facilitator = facilitator({
|
||||
serverWalletAddress: "0xYourWalletAddress",
|
||||
});
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = request.nextUrl.toString();
|
||||
const paymentData = request.headers.get("X-PAYMENT");
|
||||
export async function GET(request: Request) {
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
resourceUrl: "https://api.example.com/premium-content",
|
||||
method: "GET",
|
||||
paymentData,
|
||||
payTo: "0xYourWalletAddress",
|
||||
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
|
||||
price: "$0.01", // can also be a ERC20 token amount
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.01",
|
||||
facilitator: thirdwebX402Facilitator,
|
||||
routeConfig: {
|
||||
description: "Access to premium API content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// payment successful, execute the request
|
||||
const response = NextResponse.next();
|
||||
// optionally set the response headers back to the client
|
||||
for (const [key, value] of Object.entries(result.responseHeaders)) {
|
||||
response.headers.set(key, value);
|
||||
}
|
||||
return response;
|
||||
return Response.json({ data: "premium content" });
|
||||
} else {
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// otherwise, request payment
|
||||
return NextResponse.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid-endpoint"],
|
||||
};
|
||||
```
|
||||
|
||||
You can also use the `verifyPayment` function to verify the payment before settling it. This lets you do the work that requires payment first and then settle the payment.
|
||||
## Going Further
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||
<ArticleIconCard
|
||||
title="Client Side"
|
||||
description="Learn how to interact with x402-compatible APIs from your client applications"
|
||||
icon={TypeScriptIcon}
|
||||
href="/payments/x402/client"
|
||||
/>
|
||||
<ArticleIconCard
|
||||
title="Server Side"
|
||||
description="Accept x402 payments in your APIs with middleware or dedicated endpoints"
|
||||
icon={EngineIcon}
|
||||
href="/payments/x402/server"
|
||||
/>
|
||||
<ArticleIconCard
|
||||
title="Facilitator API"
|
||||
description="Configure the facilitator service to verify and settle payments"
|
||||
icon={EngineIcon}
|
||||
href="/payments/x402/facilitator"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,438 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage } from "@doc";
|
||||
import { Steps, Step } from "@doc";
|
||||
import PaymentFlow from "./x402-protocol-flow.png";
|
||||
|
||||
# Server Side
|
||||
|
||||
Accept x402 payments in your APIs using any x402-compatible client. Your server can verify and settle payments using thirdweb's facilitator service or any custom facilitator.
|
||||
|
||||
## Payment Flow
|
||||
|
||||
The x402 protocol follows this flow:
|
||||
|
||||
<DocImage src={PaymentFlow} alt="x402 Protocol Flow" />
|
||||
|
||||
1. **Client Request** - Client makes a request to your API
|
||||
2. **Payment Required** - Server responds with 402 and payment requirements
|
||||
3. **Client Signs** - Client signs payment authorization
|
||||
4. **Paid Request** - Client retries with payment header
|
||||
5. **Verify & Settle** - Server verifies and settles the payment
|
||||
6. **Success** - Server returns the protected content
|
||||
|
||||
## Verify vs Settle
|
||||
|
||||
You have two options for handling payments:
|
||||
|
||||
### Option 1: Settle Payment Directly
|
||||
|
||||
Use `settlePayment()` to verify and settle the payment in one step. This is the simplest approach:
|
||||
|
||||
```typescript
|
||||
const result = await settlePayment(paymentArgs);
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment settled, do the paid work
|
||||
return Response.json({ data: "premium content" });
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Verify First, Then Settle
|
||||
|
||||
Use `verifyPayment()` first, do the work, then `settlePayment()`. This is useful when:
|
||||
- The final price might be dynamic based on the work performed
|
||||
- You want to ensure the payment is valid before doing expensive work
|
||||
- You need to calculate resource usage before charging
|
||||
|
||||
```typescript
|
||||
// First verify the payment is valid
|
||||
const verifyResult = await verifyPayment(paymentArgs);
|
||||
|
||||
if (verifyResult.status !== 200) {
|
||||
return Response.json(verifyResult.responseBody, {
|
||||
status: verifyResult.status,
|
||||
headers: verifyResult.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Do the expensive work that requires payment
|
||||
const result = await doExpensiveWork();
|
||||
|
||||
// Now settle the payment based on actual usage
|
||||
const settleResult = await settlePayment({
|
||||
...paymentArgs,
|
||||
price: calculateDynamicPrice(result), // adjust price based on usage
|
||||
});
|
||||
|
||||
return Response.json(result);
|
||||
```
|
||||
|
||||
## Dedicated Endpoint Examples
|
||||
|
||||
Protect individual API endpoints with x402 payments:
|
||||
|
||||
<Tabs defaultValue="nextjs">
|
||||
<TabsList>
|
||||
<TabsTrigger value="nextjs">Next.js</TabsTrigger>
|
||||
<TabsTrigger value="express">Express</TabsTrigger>
|
||||
<TabsTrigger value="hono">Hono</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="nextjs">
|
||||
```typescript
|
||||
// app/api/premium-content/route.ts
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
// Verify and process the payment
|
||||
const result = await settlePayment({
|
||||
resourceUrl: "https://api.example.com/premium-content",
|
||||
method: "GET",
|
||||
paymentData,
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to premium API content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment verified and settled successfully
|
||||
return Response.json({ data: "premium content" });
|
||||
} else {
|
||||
// Payment required
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="express">
|
||||
```typescript
|
||||
// server.ts
|
||||
import express from "express";
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get("/api/premium", async (req, res) => {
|
||||
const result = await settlePayment({
|
||||
resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
||||
method: req.method,
|
||||
paymentData: req.headers["x-payment"],
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.05",
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to premium content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Set payment receipt headers
|
||||
Object.entries(result.responseHeaders).forEach(([key, value]) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
res.json({ message: "This is premium content!" });
|
||||
} else {
|
||||
res.status(result.status)
|
||||
.set(result.responseHeaders)
|
||||
.json(result.responseBody);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hono">
|
||||
```typescript
|
||||
// server.ts
|
||||
import { Hono } from "hono";
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/api/premium", async (c) => {
|
||||
const result = await settlePayment({
|
||||
resourceUrl: new URL(c.req.url).toString(),
|
||||
method: c.req.method,
|
||||
paymentData: c.req.header("x-payment"),
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.05",
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to premium content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
return c.json(
|
||||
{ message: "This is premium content!" },
|
||||
{ headers: result.responseHeaders }
|
||||
);
|
||||
} else {
|
||||
return c.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Middleware Examples
|
||||
|
||||
Protect multiple endpoints with a shared middleware:
|
||||
|
||||
<Tabs defaultValue="nextjs">
|
||||
<TabsList>
|
||||
<TabsTrigger value="nextjs">Next.js</TabsTrigger>
|
||||
<TabsTrigger value="express">Express</TabsTrigger>
|
||||
<TabsTrigger value="hono">Hono</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="nextjs">
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = request.nextUrl.toString();
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.01",
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
facilitator: thirdwebFacilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment successful, continue to the route
|
||||
const response = NextResponse.next();
|
||||
// Set payment receipt headers
|
||||
for (const [key, value] of Object.entries(result.responseHeaders)) {
|
||||
response.headers.set(key, value);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Payment required
|
||||
return NextResponse.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid/:path*"],
|
||||
};
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="express">
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
export async function paymentMiddleware(req, res, next) {
|
||||
const result = await settlePayment({
|
||||
resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
||||
method: req.method,
|
||||
paymentData: req.headers["x-payment"],
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.05",
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Set payment receipt headers and continue
|
||||
Object.entries(result.responseHeaders).forEach(([key, value]) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
next();
|
||||
} else {
|
||||
// Return payment required response
|
||||
res.status(result.status)
|
||||
.set(result.responseHeaders)
|
||||
.json(result.responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get("/api/premium", paymentMiddleware, (req, res) => {
|
||||
res.json({ message: "This is premium content!" });
|
||||
});
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hono">
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import type { Context, Next } from "hono";
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0x1234567890123456789012345678901234567890",
|
||||
});
|
||||
|
||||
export async function paymentMiddleware(c: Context, next: Next) {
|
||||
const result = await settlePayment({
|
||||
resourceUrl: new URL(c.req.url).toString(),
|
||||
method: c.req.method,
|
||||
paymentData: c.req.header("x-payment"),
|
||||
payTo: "0x1234567890123456789012345678901234567890",
|
||||
network: arbitrumSepolia,
|
||||
price: "$0.05",
|
||||
facilitator: thirdwebFacilitator,
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
mimeType: "application/json",
|
||||
maxTimeoutSeconds: 300,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Set payment receipt headers and continue
|
||||
Object.entries(result.responseHeaders).forEach(([key, value]) => {
|
||||
c.header(key, value);
|
||||
});
|
||||
await next();
|
||||
} else {
|
||||
return c.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get("/api/premium", paymentMiddleware, (c) => {
|
||||
return c.json({ message: "This is premium content!" });
|
||||
});
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Price Configuration
|
||||
|
||||
You can specify prices in multiple ways:
|
||||
|
||||
### USD String
|
||||
|
||||
```typescript
|
||||
price: "$0.10" // 10 cents in USD
|
||||
```
|
||||
|
||||
### ERC20 Token
|
||||
|
||||
```typescript
|
||||
price: {
|
||||
amount: "100000", // Amount in base units (100000 = 0.1 USDC with 6 decimals)
|
||||
asset: {
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Token address
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Native Token
|
||||
|
||||
Payments in native tokens are not currently supported.
|
||||
|
After Width: | Height: | Size: 376 KiB |
@@ -1,20 +1,17 @@
|
||||
import GithubSlugger from "github-slugger";
|
||||
import { FileTextIcon, FolderOpenIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { TransformedDoc } from "typedoc-better-json";
|
||||
import {
|
||||
ArticleIconCard,
|
||||
createMetadata,
|
||||
Details,
|
||||
Heading,
|
||||
} from "@/components/Document";
|
||||
import { createMetadata, Details, Heading } from "@/components/Document";
|
||||
import { Breadcrumb } from "@/components/Document/Breadcrumb";
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import type { LinkGroup, LinkMeta } from "@/components/others/Sidebar";
|
||||
import { sluggerContext } from "@/contexts/slugger";
|
||||
import type { MetadataImageIcon } from "../../../../components/Document/metadata";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
import { RootTDoc } from "./Root";
|
||||
import { getSidebarLinkGroups } from "./utils/getSidebarLinkgroups";
|
||||
import { fetchAllSlugs, getSlugToDocMap } from "./utils/slugs";
|
||||
@@ -330,10 +327,44 @@ function GroupOfLinks(props: { linkGroup: LinkGroup; level: number }) {
|
||||
accordionItemClassName="m-0"
|
||||
accordionTriggerClassName="rounded-lg"
|
||||
anchorId={slugger.slug(props.linkGroup.name)}
|
||||
headingClassName="py-0.5 text-lg"
|
||||
headingClassName="py-0.5 text-lg font-medium"
|
||||
summary={props.linkGroup.name}
|
||||
>
|
||||
<RenderLinkGroup level={props.level + 1} linkGroup={props.linkGroup} />
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
|
||||
function ArticleIconCard(props: {
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
const isExternal = props.href.startsWith("http");
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-4 rounded-xl border bg-card p-4 transition-colors hover:border-active-border",
|
||||
props.className,
|
||||
)}
|
||||
data-noindex
|
||||
href={props.href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
>
|
||||
{props.icon && (
|
||||
<div className="shrink-0">
|
||||
<div className="rounded-full p-2.5 bg-background border">
|
||||
<props.icon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="font-semibold text-base text-foreground leading-none">
|
||||
{props.title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
- For user wallets in React applications that use the SDK, you can obtain the user wallet auth token (JWT) with the [`useAuthToken()`](/references/typescript/v5/useAuthToken) hook.
|
||||
- For user wallets in TypeScript applications, you can get it by calling `wallet.getAuthToken()` on a connected [`inAppWallet()`](/references/typescript/v5/inAppWallet) or [`ecosystemWallet()`](/references/typescript/v5/ecosystemWallet).
|
||||
|
||||
<OpenApiEndpoint path="/v1/transactions" method="POST" />
|
||||
<OpenApiEndpoint path="/v1/transactions" method="POST" />
|
||||
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
### Installation
|
||||
@@ -425,47 +425,47 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
2. Enter your Client ID from the thirdweb dashboard
|
||||
3. Allowlist your game's Bundle ID on the thirdweb dashboard for security
|
||||
|
||||
### Send a Transaction
|
||||
### Send a Transaction
|
||||
|
||||
Create a new script to manage transactions:
|
||||
Create a new script to manage transactions:
|
||||
|
||||
```csharp
|
||||
using Thirdweb;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
```csharp
|
||||
using Thirdweb;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class TransactionManager : MonoBehaviour
|
||||
{
|
||||
private ThirdwebSDK sdk;
|
||||
public Text walletAddressText;
|
||||
public Button sendButton;
|
||||
public class TransactionManager : MonoBehaviour
|
||||
{
|
||||
private ThirdwebSDK sdk;
|
||||
public Text walletAddressText;
|
||||
public Button sendButton;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Client ID is set in Project Settings > Thirdweb
|
||||
sdk = new ThirdwebSDK("ethereum"); // Or any supported chain
|
||||
sendButton.onClick.AddListener(SendTransaction);
|
||||
}
|
||||
void Start()
|
||||
{
|
||||
// Client ID is set in Project Settings > Thirdweb
|
||||
sdk = new ThirdwebSDK("ethereum"); // Or any supported chain
|
||||
sendButton.onClick.AddListener(SendTransaction);
|
||||
}
|
||||
|
||||
public async void SendTransaction()
|
||||
{
|
||||
try {
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "contract-address",
|
||||
chainId: 1,
|
||||
abi: "optional-abi"
|
||||
);
|
||||
public async void SendTransaction()
|
||||
{
|
||||
try {
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "contract-address",
|
||||
chainId: 1,
|
||||
abi: "optional-abi"
|
||||
);
|
||||
|
||||
// Send a transaction
|
||||
var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount);
|
||||
Debug.Log($"Transaction receipt: {receipt}");
|
||||
}
|
||||
catch (System.Exception e) {
|
||||
Debug.LogError("Error sending transaction: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
// Send a transaction
|
||||
var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount);
|
||||
Debug.Log($"Transaction receipt: {receipt}");
|
||||
}
|
||||
catch (System.Exception e) {
|
||||
Debug.LogError("Error sending transaction: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ Example sponsored contract write request:
|
||||
|
||||
</Tabs>
|
||||
|
||||
<div className='h-6' />
|
||||
|
||||
That's it! All transactions executed by the user will be sponsored via the thirdweb infrastructure.
|
||||
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Unity SDK v6 Build Instructions | thirdweb Unity SDK",
|
||||
description:
|
||||
"Learn how to build your Unity project with the thirdweb Unity SDK.",
|
||||
});
|
||||
|
||||
# Build Instructions
|
||||
|
||||
## All Target Platforms
|
||||
|
||||
- **Player Settings:** Use IL2CPP over Mono when available.
|
||||
- **Build Settings:** Use `Smaller (faster) Builds` / `Shorter Build Time` for IL2CPP Code Generation.
|
||||
- **Stripping Level:** Set `Managed Stripping Level` to `Minimal` (`Player Settings` > `Other Settings` > `Optimization`). (Generally not a hard requirement unless using external wallets.)
|
||||
- **Strip Engine Code:** We recommend turning this off.
|
||||
|
||||
## WebGL
|
||||
|
||||
- **WebGL Template:** None enforced, feel free to customize!
|
||||
- **Compression Format:** Set to `Disabled` (`Player Settings` > `Publishing Settings`) for final builds.
|
||||
- **Testing WebGL Social Login Locally:** Host the build or run it locally with `Cross-Origin-Opener-Policy` set to `same-origin-allow-popups`.
|
||||
|
||||
Example setup for testing In-App or Ecosystem Wallet Social Login locally:
|
||||
|
||||
```javascript
|
||||
// YourWebGLOutputFolder/server.js
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const port = 8000;
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.header("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static("."));
|
||||
app.listen(port, () =>
|
||||
console.log(`Server running on http://localhost:${port}`),
|
||||
);
|
||||
|
||||
// run it with `node server.js`
|
||||
```
|
||||
|
||||
No action needed for hosted builds.
|
||||
|
||||
## Mobile
|
||||
|
||||
- **EDM4U:** Comes with the package, resolves dependencies at runtime. Use `Force Resolve` from `Assets` > `External Dependency Manager` > `Android Resolver`.
|
||||
- **Redirect Schemes:** Set custom schemes matching your bundle ID in `Plugins/Android/AndroidManifest.xml` or equivalent to ensure OAuth redirects if using social login.
|
||||
```xml
|
||||
<!-- Set your bundle id here -->
|
||||
<!-- Replace 'com.thirdweb.unitysdk' with your desired scheme -->
|
||||
<data android:scheme="com.thirdweb.unitysdk" />
|
||||
```
|
||||
|
||||
## Troubleshooting OAuth Redirects
|
||||
|
||||
If you notice issues with redirects on mobile, for instance hanging after selecting an account when logging in with Google, you did not setup OAuth properly.
|
||||
Best practice is to use a lowercase bundle id that is the same as your Unity project's application identifier, it must be present in the following places:
|
||||
- Your `ThirdwebManager` prefab `Bundle ID` field.
|
||||
- Your `Plugins/AndroidManifest.xml` scheme field as mentioned above (it doesn't have to be our default `AndroidManifest.xml`, it can be in your custom one, just add the scheme).
|
||||
- Your thirdweb dashboard project settings as one of:
|
||||
- `Bundle ID Allowlist` field (those automatically added to allowed redirect URIs).
|
||||
- In-App Wallet Configuration `Allowed redirect URIs` field.
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Details, Callout, createMetadata, ArticleIconCard } from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ThirdwebContract | Thirdweb Unity SDK",
|
||||
description:
|
||||
"Interact with any EVM smart contract through the ThirdwebContract wrapper in the Unity SDK.",
|
||||
});
|
||||
|
||||
# ThirdwebContract
|
||||
|
||||
`ThirdwebContract` is a wrapper around any EVM-compatible smart contract. It allows you to interact with the contract's functions easily, as well as prepare transactions.
|
||||
|
||||
### Instantiating a ThirdwebContract
|
||||
|
||||
```csharp
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "contract-address",
|
||||
chainId: 1,
|
||||
abi: "optional-abi"
|
||||
);
|
||||
```
|
||||
|
||||
That's it! You can now interact with the contract using the `ThirdwebContract`.
|
||||
|
||||
It contains `Read`, `Write` and `Prepare` methods to respectively read contract state, write to the contract and prepare transactions.
|
||||
|
||||
Furthermore, it contains shorthands such as `ThirdwebContract.ERC20_BalanceOf` to easily interact with common contract interfaces.
|
||||
|
||||
<ArticleIconCard
|
||||
href="/dotnet/contracts/read"
|
||||
icon={GraduationCap}
|
||||
title="Interact with Smart Contracts"
|
||||
/>
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Callout,
|
||||
Steps,
|
||||
Step,
|
||||
DocImage,
|
||||
ArticleIconCard,
|
||||
createMetadata,
|
||||
} from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Getting Started | thirdweb Unity SDK",
|
||||
description:
|
||||
"Kick off your Unity integration with the thirdweb Unity SDK and learn the essential setup steps.",
|
||||
});
|
||||
|
||||
# Getting Started
|
||||
|
||||
<Callout variant='info' title="API Key Required">
|
||||
|
||||
To get started with the Unity SDK, you will need to have a [thirdweb API Key](https://thirdweb.com/create-api-key) created. Make sure you allowlist a bundle ID to use within Unity native applications.
|
||||
|
||||
</Callout>
|
||||
|
||||
To get started, you'll need to [download and install the Unity Hub and Unity Editor](https://unity.com/download).
|
||||
|
||||
We recommend using the 2022 LTS version of Unity.
|
||||
|
||||
## Integration Steps
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Unity Package Download" >
|
||||
|
||||
Download the latest thirdweb Unity SDK package from the [releases page](https://github.com/thirdweb-dev/unity/releases).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Unity Package Import" >
|
||||
|
||||
Drag and drop the package into your Unity project.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Test it!" >
|
||||
|
||||
Play around in our example `Scene_Playground` to test out the SDK!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Build it!" >
|
||||
|
||||
Refer to the [Build Instructions](/unity/v6/build-instructions), pick a target platform and build your scene!
|
||||
|
||||
We support all platforms.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Thirdweb Manager
|
||||
|
||||
Now you're ready to use the SDK in your own scene!
|
||||
|
||||
We recommend adding a [ThirdwebManager](/unity/v6/thirdwebmanager) prefab to your scene.
|
||||
|
||||
Navigate to `Thirdweb` > `Runtime` > `Unity` > `Prefabs` and drag the `ThirdwebManager` into your scene.
|
||||
|
||||
Alternatively, use our handy quickstart installer from the top level `Tools > Thirdweb` menu.
|
||||
|
||||
<ArticleIconCard
|
||||
href="/unity/v6/thirdwebmanager"
|
||||
icon={GraduationCap}
|
||||
title="Learn more about the ThirdwebManager"
|
||||
/>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createMetadata } from "@/components/Document";
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import { PlatformSelector } from "../../../components/others/PlatformSelector";
|
||||
import { sidebar } from "./sidebar";
|
||||
|
||||
export default async function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DocLayout
|
||||
editPageButton={true}
|
||||
sideBar={sidebar}
|
||||
sidebarHeader={<PlatformSelector selected="Unity" />}
|
||||
>
|
||||
{props.children}
|
||||
</DocLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description:
|
||||
"Connect to user's wallets, interact with smart contracts, sign messages, and utilize common standards such as tokens, NFTs, marketplaces; all with built-in RPC URLs, IPFS gateways, and more.",
|
||||
image: {
|
||||
icon: "unity",
|
||||
title: "thirdweb Unity SDK",
|
||||
},
|
||||
title: "thirdweb Unity SDK",
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
|
||||
import {
|
||||
Callout,
|
||||
Details,
|
||||
createMetadata,
|
||||
} from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Unity SDK v5 to v6 Migration Guide | thirdweb Unity SDK",
|
||||
description:
|
||||
"Upgrade notes that map the thirdweb Unity SDK v5 API surface to v6, highlighting the exact code changes in ThirdwebManagerBase and wallet providers.",
|
||||
});
|
||||
|
||||
# Unity SDK v5 → v6 Migration Guide
|
||||
|
||||
Unity SDK v6 modernises wallet flows, trims unused integrations, and keeps the .NET interoperability you relied on in v5. This document compares the two `ThirdwebManagerBase` implementations and lists the code edits required to upgrade an existing project.
|
||||
|
||||
## Quick reference: what changed?
|
||||
|
||||
- **Wallet providers** – The `WalletProvider` enum now only exposes `InAppWallet`, `EcosystemWallet`, and `ReownWallet`. `PrivateKeyWallet`, `WalletConnectWallet`, and `MetaMaskWallet` handlers were removed from the manager.
|
||||
- **Reown integration** – External wallets are unified under the optional `ReownWallet` provider. Support is wrapped in `#if THIRDWEB_REOWN` guards and requires Reown AppKit in your project.
|
||||
- **Option models** – `EcosystemWalletOptions` and `InAppWalletOptions` no longer accept `legacyEncryptionKey`. `WalletOptions` gained a `ReownOptions` payload that is filled with sensible defaults when omitted.
|
||||
- **Manager internals** – `SupportedChains`, `IncludedWalletIds`, and the `_walletMapping` accessors were deleted. `ConnectWallet` now sets `ActiveWallet` directly and only persists the most recent wallet for auto-connect.
|
||||
- **Version + diagnostics** – `THIRDWEB_UNITY_SDK_VERSION` jumped to `6.0.0`, and the boot sequence logs an explicit error if `THIRDWEB_REOWN` is defined but the AppKit prefab is missing from the scene.
|
||||
|
||||
<Callout variant="info" title="Create a migration branch">
|
||||
Before updating, branch or clone your project. The v6 package deletes v5 scripts and prefabs; keeping a clean comparison helps when you reapply custom UI.
|
||||
</Callout>
|
||||
|
||||
## 1. Install v6 and remove v5 assets
|
||||
|
||||
1. Delete the v5 Thirdweb folder.
|
||||
2. Import the v6 Unity package.
|
||||
3. Remove any outdated assembly definition references.
|
||||
|
||||
<Callout variant="warning" title="Unity cache tip">
|
||||
After deleting old assemblies, run a domain reload (enter/exit Play mode) so Unity forgets the stale DLLs before you open scenes.
|
||||
</Callout>
|
||||
|
||||
## 2. Replace `ThirdwebManager` prefabs or components
|
||||
|
||||
The serialized shape of `ThirdwebManagerBase` changed between branches. Drop the new prefab into your scenes (or update custom subclasses) and clear orphaned fields.
|
||||
|
||||
```csharp
|
||||
// v5
|
||||
public enum WalletProvider {
|
||||
PrivateKeyWallet,
|
||||
InAppWallet,
|
||||
WalletConnectWallet,
|
||||
MetaMaskWallet,
|
||||
EcosystemWallet,
|
||||
}
|
||||
|
||||
// v6
|
||||
public enum WalletProvider {
|
||||
InAppWallet,
|
||||
EcosystemWallet,
|
||||
ReownWallet,
|
||||
}
|
||||
```
|
||||
|
||||
Key property changes in the component:
|
||||
|
||||
| Removed (v5) | Replacement in v6 |
|
||||
| --- | --- |
|
||||
| `ulong[] SupportedChains` | Provide supported chain IDs when you call `WalletConnectWallet` (now Reown) or manage them in code. |
|
||||
| `string[] IncludedWalletIds` | Use `ReownOptions.IncludedWalletIds` when invoking `ReownWallet`. |
|
||||
| `Dictionary<string,IThirdwebWallet> _walletMapping` and helpers (`GetWallet`, `AddWallet`, `RemoveWallet`) | Maintain your own cache if you relied on these lookups. `ConnectWallet` assigns `ActiveWallet` without tracking every address. |
|
||||
| `legacyEncryptionKey` on wallet option classes | Remove the property from any serialized scriptable objects or constructor calls. |
|
||||
|
||||
<Details summary="Auto-connect behaviour">
|
||||
`SetAutoConnectOptions` used to ignore `WalletConnectWallet`. In v6 the guard is gone, so every provider you connect (including Reown) will save its options into `PlayerPrefs`. If you prefer to opt out, disable `AutoConnectLastWallet` or clear `PlayerPrefs` after connecting.
|
||||
</Details>
|
||||
|
||||
## 3. Add (or skip) Reown support
|
||||
|
||||
Reown replaces both MetaMask and WalletConnect. The v6 manager checks for the AppKit prefab whenever the `THIRDWEB_REOWN` scripting define symbol is set.
|
||||
|
||||
```csharp
|
||||
#if THIRDWEB_REOWN
|
||||
var reownModalExists = FindObjectOfType<Reown.AppKit.Unity.AppKitCore>();
|
||||
if (!reownModalExists) {
|
||||
ThirdwebDebug.LogError("Reown AppKit not found in scene …");
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
To enable Reown:
|
||||
|
||||
1. Install the [Reown AppKit Unity package](https://docs.reown.com/appkit/unity/core/installation).
|
||||
2. Add `THIRDWEB_REOWN` to **Player Settings → Other Settings → Scripting Define Symbols** for every build target that uses external wallets.
|
||||
3. Drop the `Reown AppKit` prefab into the scenes that should surface the modal.
|
||||
|
||||
If you’re shipping only in-app or ecosystem wallets, **do not** set the scripting define and remove any references to `WalletProvider.ReownWallet`.
|
||||
|
||||
## 4. Update wallet connection code
|
||||
|
||||
Search the project for the removed enum values and update the call sites.
|
||||
|
||||
```csharp
|
||||
// v5 WalletConnect
|
||||
await ThirdwebManager.Instance.ConnectWallet(new WalletOptions(
|
||||
provider: WalletProvider.WalletConnectWallet,
|
||||
chainId: new BigInteger(421614)
|
||||
));
|
||||
|
||||
// v6 Reown
|
||||
await ThirdwebManager.Instance.ConnectWallet(new WalletOptions(
|
||||
provider: WalletProvider.ReownWallet,
|
||||
chainId: new BigInteger(421614),
|
||||
reownOptions: new ReownOptions(projectId: "YOUR_REOWN_PROJECT_ID")
|
||||
));
|
||||
```
|
||||
|
||||
Other call-site adjustments:
|
||||
|
||||
- Remove any arguments named `legacyEncryptionKey` when instantiating `InAppWalletOptions` or `EcosystemWalletOptions`.
|
||||
- Replace checks for `WalletProvider.PrivateKeyWallet` with direct usage of the .NET `PrivateKeyWallet` helpers, e.g. `await PrivateKeyWallet.Generate(client)`.
|
||||
- When you need to filter wallets in Reown, set `IncludedWalletIds`/`ExcludedWalletIds` inside the `ReownOptions` object instead of relying on the removed `IncludedWalletIds` array.
|
||||
|
||||
## 5. Refresh smart wallet upgrades and linking logic
|
||||
|
||||
`SmartWalletOptions` retained the same fields, so your upgrade code should compile unchanged. What does change is the surrounding manager state:
|
||||
|
||||
- `UpgradeToSmartWallet` no longer adds the smart wallet to a shared dictionary; it simply returns the new wallet and sets `ActiveWallet`.
|
||||
- `LinkAccount` still exists but now reads the redirect settings from `ThirdwebManagerBase`’s remaining fields. Confirm any login UI that referenced `RedirectPageHtmlOverride` still points to a valid asset.
|
||||
|
||||
```csharp
|
||||
var personalWallet = await PrivateKeyWallet.Generate(client);
|
||||
var smartWallet = await ThirdwebManager.Instance.UpgradeToSmartWallet(
|
||||
personalWallet,
|
||||
chainId: new BigInteger(421614),
|
||||
smartWalletOptions: new SmartWalletOptions(sponsorGas: true)
|
||||
);
|
||||
```
|
||||
|
||||
## 6. Clean up project assets
|
||||
|
||||
- Delete deprecated prefabs (`WalletConnectModal`, old MetaMask UI, etc.).
|
||||
- Remove scripts that referenced the dropped enum values so Unity stops warning about missing behaviours.
|
||||
- Clear serialized ScriptableObjects that kept `legacyEncryptionKey`, `SupportedChains`, or `IncludedWalletIds`.
|
||||
- Update internal docs to mention Reown and the new optional dependency model.
|
||||
|
||||
<Callout variant="warning" title="Missing script warnings">
|
||||
If Unity reports missing MonoBehaviours after the upgrade, open the affected prefab or scene, remove the broken component, and rewire it using the v6 APIs.
|
||||
</Callout>
|
||||
|
||||
## 7. Validate the migration
|
||||
|
||||
1. Open Play mode and confirm each wallet flow works (In-App, Ecosystem, and optionally Reown).
|
||||
2. Test the auto-connect flow to verify that `PlayerPrefs` stores the new `WalletOptions` payload.
|
||||
3. Build for every platform you support; Reown builds will fail if the AppKit prefab or define is missing.
|
||||
4. Run any automated gameplay/contract tests to ensure async interactions behave identically.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Explanation | Resolution |
|
||||
| --- | --- | --- |
|
||||
| `NotSupportedException: Wallet provider WalletConnectWallet is not supported.` | A v5 enum value remained in code. | Replace it with `WalletProvider.ReownWallet` or remove the branch. |
|
||||
| `Reown AppKit not found in scene …` | `THIRDWEB_REOWN` is defined but the prefab is absent. | Install AppKit and add the prefab, or remove the scripting define. |
|
||||
| `legacyEncryptionKey` compiler errors | Constructors in v6 dropped the argument. | Delete the parameter from your calls or ScriptableObject assets. |
|
||||
| Missing script warnings on UI prefabs | Old WalletConnect/MetaMask components were removed. | Delete the component and rebuild the UI against Reown or in-app wallet scripts. |
|
||||
@@ -0,0 +1,75 @@
|
||||
import { OpenSourceCard, ArticleIconCard, ArticleCard, Grid } from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
|
||||
# Unity SDK
|
||||
|
||||
The best Unity SDK to build cross-platform games on any EVM blockchain.
|
||||
|
||||
Connect to user’s wallets, interact with smart contracts, sign
|
||||
messages, and utilize common standards such as tokens, NFTs, marketplaces; all with built-in RPC URLs, IPFS gateways, Account Abstraction and more.
|
||||
|
||||
<OpenSourceCard
|
||||
title="Unity SDK"
|
||||
href="https://github.com/thirdweb-dev/unity"
|
||||
isLibrary={false}
|
||||
/>
|
||||
|
||||
## Installation
|
||||
|
||||
The Unity SDK is distributed as an [Asset Package](https://docs.unity3d.com/Manual/AssetPackages.html) allowing you to view and edit the source code.
|
||||
|
||||
It also wraps our open-source gaming-focused [.NET SDK](https://github.com/thirdweb-dev/dotnet) making version updates extremely simple containing a single DLL filechange.
|
||||
|
||||
You can download the latest version of the SDK from our [GitHub releases page](https://github.com/thirdweb-dev/unity-sdk/releases).
|
||||
|
||||
## Get Started
|
||||
|
||||
Check out the [getting started](/unity/v6/getting-started) guide to learn how to use the SDK in less than 2 minutes.
|
||||
|
||||
<ArticleIconCard
|
||||
href="/unity/v6/getting-started"
|
||||
icon={GraduationCap}
|
||||
title="Get started with the Unity SDK"
|
||||
/>
|
||||
|
||||
## Examples
|
||||
|
||||
<Grid>
|
||||
|
||||
<ArticleCard
|
||||
title="Web3 Warriors"
|
||||
description="Demo — Epic Onchain Survival Game Built in 3 Weeks. Multichain, Signless, Gasless."
|
||||
href="https://web3warriors.thirdweb.com/"
|
||||
/>
|
||||
|
||||
<ArticleCard
|
||||
title="Take Flight!"
|
||||
description="Template — Endless Runner Showcasing Seamless Onboarding."
|
||||
href="https://github.com/thirdweb-example/unity-take-flight/"
|
||||
/>
|
||||
|
||||
<ArticleCard
|
||||
title="Telegram Mini-App"
|
||||
description="Template — Bridging Telegram with Unity to interact with smart contracts."
|
||||
href="https://github.com/thirdweb-example/unity-telegram"
|
||||
/>
|
||||
|
||||
<ArticleCard
|
||||
title="Pioneer"
|
||||
description="Template — Granting signing access to an app with account abstraction to sponsor gas fees"
|
||||
href="https://github.com/thirdweb-example/unity-pioneer"
|
||||
/>
|
||||
|
||||
<ArticleCard
|
||||
title="Blockventure"
|
||||
description="Template — Blockchain RPG with Gathering, Shopping and Trading."
|
||||
href="https://github.com/thirdweb-example/unity-blockventure"
|
||||
/>
|
||||
|
||||
<ArticleCard
|
||||
title="Unity IAP Meets Blockchain"
|
||||
description="Guide — Enhancing Unity IAP with Blockchain Interactions. Frontend and Backend Integration."
|
||||
href="https://blog.thirdweb.com/guides/enhancing-unity-iap-with-blockchain-interactions/"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CodeIcon, ZapIcon } from "lucide-react";
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
|
||||
const sdkSlug = "/unity/v6";
|
||||
const walletProvidersSlug = `${sdkSlug}/wallets`;
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
links: [
|
||||
{ separator: true },
|
||||
{
|
||||
href: sdkSlug,
|
||||
name: "Overview",
|
||||
},
|
||||
{
|
||||
href: `${sdkSlug}/getting-started`,
|
||||
icon: <ZapIcon />,
|
||||
name: "Getting Started",
|
||||
},
|
||||
{
|
||||
href: "/dotnet",
|
||||
icon: <CodeIcon />,
|
||||
isCollapsible: false,
|
||||
name: "API Reference",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${sdkSlug}/thirdwebmanager`,
|
||||
name: "Thirdweb Manager",
|
||||
},
|
||||
{
|
||||
href: `${sdkSlug}/build-instructions`,
|
||||
name: "Build Instructions",
|
||||
},
|
||||
],
|
||||
name: "Core",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${walletProvidersSlug}/in-app-wallet`,
|
||||
name: "In-App Wallet",
|
||||
},
|
||||
{
|
||||
href: `${walletProvidersSlug}/ecosystem-wallet`,
|
||||
name: "Ecosystem Wallet",
|
||||
},
|
||||
{
|
||||
href: `${walletProvidersSlug}/account-abstraction`,
|
||||
name: "Account Abstraction",
|
||||
},
|
||||
{
|
||||
href: `${walletProvidersSlug}/reown`,
|
||||
name: "External Wallet (Reown)",
|
||||
},
|
||||
],
|
||||
name: "Onboarding Users",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${sdkSlug}/contracts`,
|
||||
name: "Interacting with Contracts",
|
||||
},
|
||||
{
|
||||
href: "/dotnet/contracts/extensions",
|
||||
name: "Contract Extensions",
|
||||
},
|
||||
],
|
||||
name: "Onchain Interactions",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
href: `${sdkSlug}/migration-guide`,
|
||||
name: "Migrate from v5",
|
||||
},
|
||||
],
|
||||
name: "Unity SDK",
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
import { DocImage, createMetadata } from "@doc";
|
||||
import thirdwebmanager_client from "./thirdwebmanager_client.png";
|
||||
import thirdwebmanager_preferences from "./thirdwebmanager_preferences.png";
|
||||
import thirdwebmanager_misc from "./thirdwebmanager_misc.png";
|
||||
import thirdwebmanager_debug from "./thirdwebmanager_debug.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Thirdweb Manager | thirdweb Unity SDK",
|
||||
description:
|
||||
"ThirdwebManager is a prefab that provides a convenient way to instantiate the ThirdwebClient, and contains helper functions to create contracts and wallets. Add the prefab to your scene and the SDK will persist throughout your game's lifecycle.",
|
||||
});
|
||||
|
||||
# Thirdweb Manager
|
||||
|
||||
The `ThirdwebManager` is a prefab that provides a convenient way to instantiate the [ThirdwebClient](/dotnet/client), and contains helper functions to create contracts and wallets.
|
||||
Add the prefab to your scene and the client will persist throughout your game's lifecycle, keeping track of your connected wallets.
|
||||
|
||||
It is entirely optional, and you can opt to use the [.NET SDK](/dotnet) directly if you prefer to do so.
|
||||
|
||||
If you are wrapping the SDK, we recommend making your own Manager inspired by `ThirdwebManager.cs` that extends `ThirdwebManagerBase.cs`, specifically for `ThirdwebClient` initialization. Every method and property is virtual and can be overridden.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure `ThirdwebManager` through the Unity Inspector window.
|
||||
|
||||
Below is a list of all the settings you can adjust.
|
||||
|
||||
### Client
|
||||
|
||||
<DocImage src={thirdwebmanager_client} />
|
||||
|
||||
This section configures the `ThirdwebClient` instance that the manager creates at runtime:
|
||||
|
||||
- `Client ID`: Required thirdweb [API Key](https://thirdweb.com/create-api-key). Without it, `ThirdwebManager` refuses to initialize and logs an error.
|
||||
- `Bundle ID`: Optional identifier used for native redirect URIs. If left blank, the manager falls back to `Application.identifier` (or `com.<Company>.<Product>` when no identifier is set).
|
||||
- `Create API Key`: Opens the dashboard so you can generate a fresh API key.
|
||||
|
||||
### Preferences
|
||||
|
||||
<DocImage src={thirdwebmanager_preferences} />
|
||||
|
||||
Use these toggles to shape how the manager boots and reports information:
|
||||
|
||||
- `Initialize On Awake`: Automatically calls `Initialize()` during `Awake`. Disable it if you need to delay initialization or swap credentials at runtime.
|
||||
- `Show Debug Logs`: Mirrors `ThirdwebDebug.IsEnabled`, enabling verbose diagnostics during development.
|
||||
- `Auto-Connect Last Wallet`: Persists the most recent `WalletOptions` to `PlayerPrefs` and reconnects on startup (including smart-wallet upgrades). Errors in this flow are swallowed and logged.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
<DocImage src={thirdwebmanager_misc} />
|
||||
|
||||
Fine-tune the runtime behaviour of authentication and networking here:
|
||||
|
||||
- `RPC Overrides`: Per-chain RPC endpoints stored as `ChainId` → `URL` pairs. When populated, these replace the default thirdweb RPCs for the matching chains.
|
||||
- `OAuth Redirect Page HTML Override`: Multiline HTML snippet used by the in-app and ecosystem OAuth flows (including external browser handoffs). Provide custom markup only if you know the full redirect requirements.
|
||||
|
||||
### Debug
|
||||
|
||||
<DocImage src={thirdwebmanager_debug} />
|
||||
|
||||
Quick utilities exposed by the custom inspector:
|
||||
|
||||
- `Log Active Wallet Info`: Available in Play Mode; prints the current `ActiveWallet` type and address to the Unity Console.
|
||||
- `Open Documentation`: Launches the hosted Unity SDK documentation in your default browser.
|
||||
|
||||
## Interacting with the ThirdwebManager
|
||||
|
||||
Once your `ThirdwebManager` is set up, you can interact with it using the following methods:
|
||||
|
||||
### Initialize (If not set to `Initialize On Awake`)
|
||||
|
||||
```csharp
|
||||
ThirdwebManager.Instance.Initialize();
|
||||
```
|
||||
|
||||
Initializes the SDK with the settings specified in the Unity Inspector.
|
||||
|
||||
### ConnectWallet
|
||||
|
||||
```csharp
|
||||
var walletOptions = new WalletOptions(
|
||||
provider: WalletProvider.EcosystemWallet,
|
||||
chainId: 1,
|
||||
inAppWalletOptions: new InAppWalletOptions(authprovider: AuthProvider.Guest)
|
||||
);
|
||||
var wallet = await ThirdwebManager.Instance.ConnectWallet(walletOptions);
|
||||
var address = await wallet.GetAddress();
|
||||
ThirdwebDebug.Log($"Connected wallet address: {address}");
|
||||
```
|
||||
|
||||
Connects a wallet based on the specified `WalletOptions` and returns an `IThirdwebWallet` instance that can be used to interact with the blockchain.
|
||||
|
||||
### UpgradeToSmartWallet
|
||||
|
||||
```csharp
|
||||
var smartWallet = await ThirdwebManager.Instance.UpgradeToSmartWallet(wallet, chainId, smartWalletOptions);
|
||||
```
|
||||
|
||||
Upgrades the specified wallet to a `SmartWallet`, returning a `SmartWallet` instance.
|
||||
|
||||
### ActiveWallet
|
||||
|
||||
```csharp
|
||||
var activeWallet = ThirdwebManager.Instance.ActiveWallet;
|
||||
var address = await activeWallet.GetAddress();
|
||||
```
|
||||
|
||||
Returns the last connected wallet as an `IThirdwebWallet` instance, or `null` if no wallet is connected.
|
||||
|
||||
### GetContract
|
||||
|
||||
```csharp
|
||||
var contract = await ThirdwebManager.Instance.GetContract("contract-address", chainId, "optional-contract-abi");
|
||||
var result = await contract.Read<string>("name");
|
||||
```
|
||||
|
||||
Returns a [ThirdwebContract](/dotnet/contracts/create) instance that can be used to interact with a smart contract.
|
||||
|
||||
### LinkAccount
|
||||
|
||||
```csharp
|
||||
var linkedAccounts = await ThirdwebManager.Instance.LinkAccount(mainWallet, walletToLink, otp, chainId, jwtOrPayload);
|
||||
```
|
||||
|
||||
Links another `InAppWallet` or `EcosystemWallet` account to the main wallet and returns a list of linked accounts, allowing you to login with either authentication method later.
|
||||
|
||||
## Child Prefabs
|
||||
|
||||
### DefaultOTPModal
|
||||
|
||||
The helper modal that is displayed when using the `ThirdwebManager`'s `ConnectWallet` function with an auth method that requires an OTP, such as Email or Phone login.
|
||||
|
||||
It can be replaced with a custom modal that extends `AbstractOTPVerifyModal` to customize the OTP verification process.
|
||||
|
||||
## What Now?
|
||||
|
||||
Explore the [.NET SDK](/dotnet) to learn more about interacting with smart contracts, wallets, storage, RPC, account abstraction, and more.
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 24 KiB |